繼承和原型鏈

在程式設計中,繼承指的是特性從父級傳遞給子級,以便新的程式碼塊可以重用並基於現有程式碼的功能進行構建。JavaScript 透過使用物件來實現繼承。每個物件都有一條指向另一個物件的內部連結,該物件稱為其原型。該原型物件又有它自己的原型,依此類推,直到達到一個以 null 作為其原型的物件。根據定義,null 沒有原型,並充當這個原型鏈中的最終連結。可以在執行時修改原型鏈的任何成員,甚至替換原型,因此像靜態排程這樣的概念在 JavaScript 中不存在。

對於有類式語言(如 Java 或 C++)經驗的開發人員來說,JavaScript 有點令人困惑,因為它具有動態性並且沒有靜態型別。雖然這種困惑通常被認為是 JavaScript 的弱點之一,但原型繼承模型本身實際上比經典模型更強大。例如,在原型模型之上構建一個經典模型是相當簡單的——這就是的實現方式。

儘管類現在被廣泛採用併成為 JavaScript 中的新正規化,但類並沒有帶來新的繼承模式。雖然類抽象掉了大部分原型機制,但理解原型在底層是如何工作的仍然很有用。

透過原型鏈繼承

繼承屬性

JavaScript 物件是屬性(被稱為自有屬性)的動態“袋子”。JavaScript 物件有一個指向原型物件的連結。當嘗試訪問物件的屬性時,該屬性不僅會在物件本身上查詢,還會在物件的原型上、原型的原型上查詢,依此類推,直到找到具有匹配名稱的屬性或到達原型鏈的末尾。

注意:遵循 ECMAScript 標準,someObject.[[Prototype]] 符號用於表示 someObject 的原型。[[Prototype]] 內部槽位可以透過 Object.getPrototypeOf()Object.setPrototypeOf() 函式分別訪問和修改。這等效於 JavaScript 訪問器 __proto__,它不是標準但實際上被許多 JavaScript 引擎實現。為了避免混淆並保持簡潔,在我們的表示法中,我們將避免使用 obj.__proto__,而是使用 obj.[[Prototype]]。這對應於 Object.getPrototypeOf(obj)

它不應與函式的 func.prototype 屬性混淆,後者指定當給定函式用作建構函式時,將分配給該函式建立的所有物件例項[[Prototype]]。我們將在稍後部分討論建構函式的 prototype 屬性。

有幾種方法可以指定物件的 [[Prototype]],這些方法列在後面的部分。目前,我們將使用 __proto__ 語法進行說明。值得注意的是,{ __proto__: ... } 語法與 obj.__proto__ 訪問器不同:前者是標準的,未棄用。

在像 { a: 1, b: 2, __proto__: c } 這樣的物件字面量中,值 c(它必須是 null 或另一個物件)將成為字面量所表示物件的 [[Prototype]],而像 ab 這樣的其他鍵將成為物件的自有屬性。這種語法讀起來非常自然,因為 [[Prototype]] 只是物件的“內部屬性”。

以下是嘗試訪問屬性時發生的情況

js
const o = {
  a: 1,
  b: 2,
  // __proto__ sets the [[Prototype]]. It's specified here
  // as another object literal.
  __proto__: {
    b: 3,
    c: 4,
  },
};

// o.[[Prototype]] has properties b and c.
// o.[[Prototype]].[[Prototype]] is Object.prototype (we will explain
// what that means later).
// Finally, o.[[Prototype]].[[Prototype]].[[Prototype]] is null.
// This is the end of the prototype chain, as null,
// by definition, has no [[Prototype]].
// Thus, the full prototype chain looks like:
// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null

console.log(o.a); // 1
// Is there an 'a' own property on o? Yes, and its value is 1.

console.log(o.b); // 2
// Is there a 'b' own property on o? Yes, and its value is 2.
// The prototype also has a 'b' property, but it's not visited.
// This is called Property Shadowing

console.log(o.c); // 4
// Is there a 'c' own property on o? No, check its prototype.
// Is there a 'c' own property on o.[[Prototype]]? Yes, its value is 4.

console.log(o.d); // undefined
// Is there a 'd' own property on o? No, check its prototype.
// Is there a 'd' own property on o.[[Prototype]]? No, check its prototype.
// o.[[Prototype]].[[Prototype]] is Object.prototype and
// there is no 'd' property by default, check its prototype.
// o.[[Prototype]].[[Prototype]].[[Prototype]] is null, stop searching,
// no property found, return undefined.

為物件設定屬性會建立一個自有屬性。獲取和設定行為規則的唯一例外是當它被getter 或 setter攔截時。

同樣,您可以建立更長的原型鏈,並且將在所有這些原型鏈上查詢屬性。

js
const o = {
  a: 1,
  b: 2,
  // __proto__ sets the [[Prototype]]. It's specified here
  // as another object literal.
  __proto__: {
    b: 3,
    c: 4,
    __proto__: {
      d: 5,
    },
  },
};

// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null

console.log(o.d); // 5

繼承“方法”

JavaScript 沒有類式語言定義的那種“方法”。在 JavaScript 中,任何函式都可以作為屬性新增到物件中。繼承的函式與其他任何屬性一樣,包括上面所示的屬性遮蔽(在這種情況下,是一種方法重寫的形式)。

當執行繼承的函式時,this 的值指向繼承物件,而不是函式作為自有屬性的原型物件。

js
const parent = {
  value: 2,
  method() {
    return this.value + 1;
  },
};

console.log(parent.method()); // 3
// When calling parent.method in this case, 'this' refers to parent

// child is an object that inherits from parent
const child = {
  __proto__: parent,
};
console.log(child.method()); // 3
// When child.method is called, 'this' refers to child.
// So when child inherits the method of parent,
// The property 'value' is sought on child. However, since child
// doesn't have an own property called 'value', the property is
// found on the [[Prototype]], which is parent.value.

child.value = 4; // assign the value 4 to the property 'value' on child.
// This shadows the 'value' property on parent.
// The child object now looks like:
// { value: 4, __proto__: { value: 2, method: [Function] } }
console.log(child.method()); // 5
// Since child now has the 'value' property, 'this.value' means
// child.value instead

建構函式

原型的強大之處在於,如果一套屬性應該存在於每個例項上,我們可以重用它們——尤其是對於方法。假設我們要建立一系列盒子,每個盒子都是一個物件,其中包含一個可以透過 getValue 函式訪問的值。一個幼稚的實現是

js
const boxes = [
  { value: 1, getValue() { return this.value; } },
  { value: 2, getValue() { return this.value; } },
  { value: 3, getValue() { return this.value; } },
];

這很糟糕,因為每個例項都有自己的執行相同操作的函式屬性,這是冗餘和不必要的。相反,我們可以將 getValue 移動到所有盒子的 [[Prototype]]

js
const boxPrototype = {
  getValue() {
    return this.value;
  },
};

const boxes = [
  { value: 1, __proto__: boxPrototype },
  { value: 2, __proto__: boxPrototype },
  { value: 3, __proto__: boxPrototype },
];

透過這種方式,所有盒子的 getValue 方法都將引用同一個函式,從而降低記憶體使用。然而,手動為每次物件建立繫結 __proto__ 仍然非常不方便。這就是我們使用建構函式的時候,它會自動為每個製造的物件設定 [[Prototype]]。建構函式是用 new 呼叫的函式。

js
// A constructor function
function Box(value) {
  this.value = value;
}

// Properties all boxes created from the Box() constructor
// will have
Box.prototype.getValue = function () {
  return this.value;
};

const boxes = [new Box(1), new Box(2), new Box(3)];

我們說 new Box(1) 是從 Box 建構函式建立的例項Box.prototype 與我們之前建立的 boxPrototype 物件沒有太大區別——它只是一個普通物件。從建構函式建立的每個例項都將自動擁有建構函式的 prototype 屬性作為其 [[Prototype]]——也就是說,Object.getPrototypeOf(new Box()) === Box.prototypeConstructor.prototype 預設有一個自有屬性:constructor,它引用建構函式本身——也就是說,Box.prototype.constructor === Box。這允許從任何例項訪問原始建構函式。

注意:如果建構函式返回一個非原始值,那麼該值將成為 new 表示式的結果。在這種情況下,[[Prototype]] 可能無法正確繫結——但這在實踐中不常發生。

上述建構函式可以用重寫為

js
class Box {
  constructor(value) {
    this.value = value;
  }

  // Methods are created on Box.prototype
  getValue() {
    return this.value;
  }
}

類是建構函式的語法糖,這意味著您仍然可以操作 Box.prototype 來改變所有例項的行為。然而,由於類被設計為底層原型機制的抽象,為了充分演示原型的工作原理,本教程將使用更輕量級的建構函式語法。

由於 Box.prototype 引用與所有例項的 [[Prototype]] 相同的物件,我們可以透過修改 Box.prototype 來改變所有例項的行為。

js
function Box(value) {
  this.value = value;
}
Box.prototype.getValue = function () {
  return this.value;
};
const box = new Box(1);

// Mutate Box.prototype after an instance has already been created
Box.prototype.getValue = function () {
  return this.value + 1;
};
box.getValue(); // 2

推論是,重新分配 Constructor.prototype (Constructor.prototype = ...) 是一個壞主意,原因有二

  • 重新賦值之前建立的例項的 [[Prototype]] 現在引用了一個與重新賦值之後建立的例項的 [[Prototype]] 不同的物件——修改一個的 [[Prototype]] 不再修改另一個。
  • 除非您手動重新設定 constructor 屬性,否則建構函式無法再從 instance.constructor 追溯,這可能會違反使用者預期。一些內建操作也會讀取 constructor 屬性,如果未設定,它們可能無法按預期工作。

Constructor.prototype 僅在構造例項時有用。它與 Constructor.[[Prototype]] 無關,後者是建構函式自身的原型,即 Function.prototype——也就是說,Object.getPrototypeOf(Constructor) === Function.prototype

字面量的隱式建構函式

JavaScript 中的一些字面量語法會建立隱式設定 [[Prototype]] 的例項。例如

js
// Object literals (without the `__proto__` key) automatically
// have `Object.prototype` as their `[[Prototype]]`
const object = { a: 1 };
Object.getPrototypeOf(object) === Object.prototype; // true

// Array literals automatically have `Array.prototype` as their `[[Prototype]]`
const array = [1, 2, 3];
Object.getPrototypeOf(array) === Array.prototype; // true

// RegExp literals automatically have `RegExp.prototype` as their `[[Prototype]]`
const regexp = /abc/;
Object.getPrototypeOf(regexp) === RegExp.prototype; // true

我們可以將它們“去語法糖化”為它們的建構函式形式。

js
const array = new Array(1, 2, 3);
const regexp = new RegExp("abc");

例如,像 map() 這樣的“陣列方法”只是在 Array.prototype 上定義的方法,這就是它們在所有陣列例項上自動可用的原因。

警告:曾經有一種普遍存在的錯誤特性——擴充套件 Object.prototype 或其他內建原型。這種錯誤特性的一個例子是,定義 Array.prototype.myMethod = function () {...},然後在所有陣列例項上使用 myMethod

這種錯誤特性被稱為猴子補丁。進行猴子補丁會冒前向相容性的風險,因為如果語言將來新增此方法但具有不同的簽名,您的程式碼將崩潰。它導致了像 SmooshGate 這樣的事件,並且對於 JavaScript 的發展來說可能是一個巨大的麻煩,因為 JavaScript 試圖“不破壞 Web”。

擴充套件內建原型的**唯一**好理由是向後移植較新 JavaScript 引擎的功能,例如 Array.prototype.forEach

有趣的是,由於歷史原因,一些內建建構函式的 prototype 屬性本身就是例項。例如,Number.prototype 是數字 0,Array.prototype 是一個空陣列,RegExp.prototype/(?:)/

js
Number.prototype + 1; // 1
Array.prototype.map((x) => x + 1); // []
String.prototype + "a"; // "a"
RegExp.prototype.source; // "(?:)"
Function.prototype(); // Function.prototype is a no-op function by itself

然而,對於使用者定義的建構函式,以及像 Map 這樣的現代建構函式,情況並非如此。

js
Map.prototype.get(1);
// Uncaught TypeError: get method called on incompatible Map.prototype

構建更長的繼承鏈

Constructor.prototype 屬性將成為建構函式例項的 [[Prototype]],原樣不動——包括 Constructor.prototype 自身的 [[Prototype]]。預設情況下,Constructor.prototype 是一個普通物件——也就是說,Object.getPrototypeOf(Constructor.prototype) === Object.prototype。唯一的例外是 Object.prototype 本身,其 [[Prototype]]null——也就是說,Object.getPrototypeOf(Object.prototype) === null。因此,一個典型的建構函式將構建以下原型鏈

js
function Constructor() {}

const obj = new Constructor();
// obj ---> Constructor.prototype ---> Object.prototype ---> null

為了構建更長的原型鏈,我們可以透過 Object.setPrototypeOf() 函式設定 Constructor.prototype[[Prototype]]

js
function Base() {}
function Derived() {}
// Set the `[[Prototype]]` of `Derived.prototype`
// to `Base.prototype`
Object.setPrototypeOf(Derived.prototype, Base.prototype);

const obj = new Derived();
// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null

在類的術語中,這相當於使用 extends 語法。

js
class Base {}
class Derived extends Base {}

const obj = new Derived();
// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null

您可能還會看到一些遺留程式碼使用 Object.create() 來構建繼承鏈。然而,由於這會重新分配 prototype 屬性並刪除 constructor 屬性,因此它更容易出錯,而且如果建構函式尚未建立任何例項,效能提升可能不明顯。

js
function Base() {}
function Derived() {}
// Re-assigns `Derived.prototype` to a new object
// with `Base.prototype` as its `[[Prototype]]`
// DON'T DO THIS — use Object.setPrototypeOf to mutate it instead
Derived.prototype = Object.create(Base.prototype);

檢查原型:深入探討

讓我們更詳細地瞭解幕後發生的事情。

在 JavaScript 中,如上所述,函式可以擁有屬性。所有函式都有一個名為 prototype 的特殊屬性。請注意,以下程式碼是獨立的(可以安全地假設網頁上除了以下程式碼之外沒有其他 JavaScript)。為了獲得最佳學習體驗,強烈建議您開啟控制檯,導航到“控制檯”選項卡,複製並貼上以下 JavaScript 程式碼,然後按 Enter/Return 鍵執行它。(大多數 Web 瀏覽器的開發者工具都包含控制檯。有關 Firefox 開發者工具Chrome 開發者工具Edge 開發者工具的更多資訊可用。)

js
function doSomething() {}
console.log(doSomething.prototype);
// It does not matter how you declare the function; a
// function in JavaScript will always have a default
// prototype property — with one exception: an arrow
// function doesn't have a default prototype property:
const doSomethingFromArrowFunction = () => {};
console.log(doSomethingFromArrowFunction.prototype);

如上所示,doSomething() 有一個預設的 prototype 屬性,如控制檯所示。執行此程式碼後,控制檯應顯示一個類似於此的物件。

{
  constructor: ƒ doSomething(),
  [[Prototype]]: {
    constructor: ƒ Object(),
    hasOwnProperty: ƒ hasOwnProperty(),
    isPrototypeOf: ƒ isPrototypeOf(),
    propertyIsEnumerable: ƒ propertyIsEnumerable(),
    toLocaleString: ƒ toLocaleString(),
    toString: ƒ toString(),
    valueOf: ƒ valueOf()
  }
}

注意:Chrome 控制檯使用 [[Prototype]] 表示物件的原型,遵循規範的術語;Firefox 使用 <prototype>。為了一致性,我們將使用 [[Prototype]]

我們可以向 doSomething() 的原型新增屬性,如下所示。

js
function doSomething() {}
doSomething.prototype.foo = "bar";
console.log(doSomething.prototype);

結果是

{
  foo: "bar",
  constructor: ƒ doSomething(),
  [[Prototype]]: {
    constructor: ƒ Object(),
    hasOwnProperty: ƒ hasOwnProperty(),
    isPrototypeOf: ƒ isPrototypeOf(),
    propertyIsEnumerable: ƒ propertyIsEnumerable(),
    toLocaleString: ƒ toLocaleString(),
    toString: ƒ toString(),
    valueOf: ƒ valueOf()
  }
}

我們現在可以使用 new 運算子根據此原型建立 doSomething() 的例項。要使用 new 運算子,像往常一樣呼叫函式,但前面加上 new。使用 new 運算子呼叫函式會返回一個作為函式例項的物件。然後可以將屬性新增到此物件中。

嘗試以下程式碼

js
function doSomething() {}
doSomething.prototype.foo = "bar"; // add a property onto the prototype
const doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // add a property onto the object
console.log(doSomeInstancing);

這將導致類似於以下內容的輸出

{
  prop: "some value",
  [[Prototype]]: {
    foo: "bar",
    constructor: ƒ doSomething(),
    [[Prototype]]: {
      constructor: ƒ Object(),
      hasOwnProperty: ƒ hasOwnProperty(),
      isPrototypeOf: ƒ isPrototypeOf(),
      propertyIsEnumerable: ƒ propertyIsEnumerable(),
      toLocaleString: ƒ toLocaleString(),
      toString: ƒ toString(),
      valueOf: ƒ valueOf()
    }
  }
}

如上所示,doSomeInstancing[[Prototype]]doSomething.prototype。但這有什麼用呢?當您訪問 doSomeInstancing 的屬性時,執行時首先會檢視 doSomeInstancing 是否具有該屬性。

如果 doSomeInstancing 不具有該屬性,則執行時會在 doSomeInstancing.[[Prototype]] (也就是 doSomething.prototype) 中查詢該屬性。如果 doSomeInstancing.[[Prototype]] 具有正在查詢的屬性,則使用 doSomeInstancing.[[Prototype]] 上的該屬性。

否則,如果 doSomeInstancing.[[Prototype]] 不具有該屬性,則會檢查 doSomeInstancing.[[Prototype]].[[Prototype]] 是否具有該屬性。預設情況下,任何函式的 prototype 屬性的 [[Prototype]] 都是 Object.prototype。因此,然後會在 doSomeInstancing.[[Prototype]].[[Prototype]] (也就是 doSomething.prototype.[[Prototype]] (也就是 Object.prototype)) 中查詢正在搜尋的屬性。

如果屬性在 doSomeInstancing.[[Prototype]].[[Prototype]] 中未找到,則會查詢 doSomeInstancing.[[Prototype]].[[Prototype]].[[Prototype]]。然而,存在一個問題:doSomeInstancing.[[Prototype]].[[Prototype]].[[Prototype]] 不存在,因為 Object.prototype.[[Prototype]]null。然後,並且只有在整個 [[Prototype]] 原型鏈都被查詢之後,執行時才會斷言該屬性不存在,並得出該屬性的值為 undefined 的結論。

讓我們嘗試在控制檯中輸入更多程式碼

js
function doSomething() {}
doSomething.prototype.foo = "bar";
const doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log("doSomeInstancing.prop:     ", doSomeInstancing.prop);
console.log("doSomeInstancing.foo:      ", doSomeInstancing.foo);
console.log("doSomething.prop:          ", doSomething.prop);
console.log("doSomething.foo:           ", doSomething.foo);
console.log("doSomething.prototype.prop:", doSomething.prototype.prop);
console.log("doSomething.prototype.foo: ", doSomething.prototype.foo);

這將導致以下結果

doSomeInstancing.prop:      some value
doSomeInstancing.foo:       bar
doSomething.prop:           undefined
doSomething.foo:            undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo:  bar

建立和修改原型鏈的不同方法

我們遇到了許多建立物件和更改其原型鏈的方法。我們將系統地總結不同的方法,比較每種方法的優缺點。

用語法結構建立的物件

js
const o = { a: 1 };
// The newly created object o has Object.prototype as its [[Prototype]]
// Object.prototype has null as its [[Prototype]].
// o ---> Object.prototype ---> null

const b = ["yo", "sup", "?"];
// Arrays inherit from Array.prototype
// (which has methods indexOf, forEach, etc.)
// The prototype chain looks like:
// b ---> Array.prototype ---> Object.prototype ---> null

function f() {
  return 2;
}
// Functions inherit from Function.prototype
// (which has methods call, bind, etc.)
// f ---> Function.prototype ---> Object.prototype ---> null

const p = { b: 2, __proto__: o };
// It is possible to point the newly created object's [[Prototype]] to
// another object via the __proto__ literal property. (Not to be confused
// with Object.prototype.__proto__ accessors)
// p ---> o ---> Object.prototype ---> null

當在物件初始化器中使用 __proto__ 鍵時,將 __proto__ 鍵指向非物件的值只會默默地失敗,而不會丟擲異常。與 Object.prototype.__proto__ setter 相反,物件字面量初始化器中的 __proto__ 是標準化的且經過最佳化的,甚至可以比 Object.create 具有更好的效能。在建立時在物件上宣告額外的自有屬性比 Object.create 更符合人體工程學。

使用建構函式

js
function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype.addVertex = function (v) {
  this.vertices.push(v);
};

const g = new Graph();
// g is an object with own properties 'vertices' and 'edges'.
// g.[[Prototype]] is the value of Graph.prototype when new Graph() is executed.

建構函式在 JavaScript 早期就已經可用。因此,它非常快速、非常標準且非常適合 JIT 最佳化。然而,它也很難“正確地做”,因為以這種方式新增的方法預設是可列舉的,這與類語法或內建方法的行為不一致。如前所述,做更長的繼承鏈也容易出錯。

使用 Object.create()

呼叫 Object.create() 會建立一個新物件。此物件的 [[Prototype]] 是函式的第一個引數

js
const a = { a: 1 };
// a ---> Object.prototype ---> null

const b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (inherited)

const c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

const d = Object.create(null);
// d ---> null (d is an object that has null directly as its prototype)
console.log(d.hasOwnProperty);
// undefined, because d doesn't inherit from Object.prototype

與物件初始化器中的 __proto__ 鍵類似,Object.create() 允許在建立時直接設定物件的原型,這使得執行時可以進一步最佳化物件。它還允許使用 Object.create(null) 建立具有 null 原型的物件。Object.create() 的第二個引數允許您精確指定新物件中每個屬性的屬性,這可能是一把雙刃劍

  • 它允許您在物件建立期間建立不可列舉的屬性等,這在物件字面量中是不可能的。
  • 它比物件字面量更冗長且容易出錯。
  • 它可能比物件字面量慢,尤其是在建立許多屬性時。

使用類

js
class Rectangle {
  constructor(height, width) {
    this.name = "Rectangle";
    this.height = height;
    this.width = width;
  }
}

class FilledRectangle extends Rectangle {
  constructor(height, width, color) {
    super(height, width);
    this.name = "Filled rectangle";
    this.color = color;
  }
}

const filledRectangle = new FilledRectangle(5, 10, "blue");
// filledRectangle ---> FilledRectangle.prototype ---> Rectangle.prototype ---> Object.prototype ---> null

在定義複雜的繼承結構時,類提供了最高的程式碼可讀性和可維護性。私有元素是原型繼承中沒有簡單替代方案的功能。然而,類不如傳統建構函式最佳化,並且在舊環境中不受支援。

使用 Object.setPrototypeOf()

雖然上述所有方法都會在物件建立時設定原型鏈,但 Object.setPrototypeOf() 允許修改現有物件的 [[Prototype]] 內部屬性。它甚至可以強制將原型應用於使用 Object.create(null) 建立的無原型物件,或者透過將其設定為 null 來刪除物件的原型。

js
const obj = { a: 1 };
const anotherObj = { b: 2 };
Object.setPrototypeOf(obj, anotherObj);
// obj ---> anotherObj ---> Object.prototype ---> null

但是,如果可能,您應該在建立時設定原型,因為動態設定原型會破壞引擎對原型鏈所做的所有最佳化。它可能會導致某些引擎重新編譯您的程式碼以進行去最佳化,以使其符合規範。

使用 __proto__ 訪問器

所有物件都繼承了 Object.prototype.__proto__ setter,它可以用來設定現有物件的 [[Prototype]](如果物件的 __proto__ 鍵未被覆蓋)。

警告: Object.prototype.__proto__ 訪問器是非標準且已棄用的。您幾乎總是應該使用 Object.setPrototypeOf 代替。

js
const obj = {};
// DON'T USE THIS: for example only.
obj.__proto__ = { barProp: "bar val" };
obj.__proto__.__proto__ = { fooProp: "foo val" };
console.log(obj.fooProp);
console.log(obj.barProp);

Object.setPrototypeOf 相比,將 __proto__ 設定為非物件會靜默失敗而不會丟擲異常。它還具有稍微更好的瀏覽器支援。然而,它是非標準且已棄用的。您幾乎總是應該使用 Object.setPrototypeOf 代替。

效能

原型鏈上較高位置的屬性的查詢時間可能對效能產生負面影響,這在效能至關重要的程式碼中可能很重要。此外,嘗試訪問不存在的屬性將始終遍歷整個原型鏈。

此外,當遍歷物件的屬性時,原型鏈上的每個可列舉屬性都將被列舉。要檢查物件是否在自身上定義了屬性,而不是在其原型鏈上的某個位置,需要使用 hasOwnPropertyObject.hasOwn 方法。除了那些 [[Prototype]]null 的物件之外,所有物件都從 Object.prototype 繼承 hasOwnProperty——除非它在原型鏈的更深層被覆蓋。為了給您一個具體的例子,讓我們使用上面的圖表示例程式碼來說明它

js
function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype.addVertex = function (v) {
  this.vertices.push(v);
};

const g = new Graph();
// g ---> Graph.prototype ---> Object.prototype ---> null

g.hasOwnProperty("vertices"); // true
Object.hasOwn(g, "vertices"); // true

g.hasOwnProperty("nope"); // false
Object.hasOwn(g, "nope"); // false

g.hasOwnProperty("addVertex"); // false
Object.hasOwn(g, "addVertex"); // false

Object.getPrototypeOf(g).hasOwnProperty("addVertex"); // true

注意:僅僅檢查屬性是否為 undefined 是**不夠的**。屬性可能確實存在,但其值恰好被設定為 undefined

總結

對於來自 Java 或 C++ 的開發人員來說,JavaScript 可能有點令人困惑,因為它都是動態的、都是執行時的,並且完全沒有靜態型別。一切要麼是物件(例項),要麼是函式(建構函式),甚至函式本身都是 Function 建構函式的例項。即使是作為語法結構的“類”,在執行時也只是建構函式。

JavaScript 中的所有建構函式都有一個特殊的屬性叫做 prototype,它與 new 運算子一起使用。原型物件的引用被複制到新例項的內部 [[Prototype]] 屬性中。例如,當您執行 const a1 = new A() 時,JavaScript(在記憶體中建立物件並在執行函式 A() 並將其 this 定義給它之前)會設定 a1.[[Prototype]] = A.prototype。然後,當您訪問例項的屬性時,JavaScript 首先檢查它們是否存在於該物件本身,如果不存在,則在 [[Prototype]] 中查詢。[[Prototype]]遞迴查詢的,即 a1.doSomethingObject.getPrototypeOf(a1).doSomethingObject.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething 等,直到找到或 Object.getPrototypeOf 返回 null。這意味著定義在 prototype 上的所有屬性都由所有例項有效地共享,您甚至可以在以後更改 prototype 的部分,並讓這些更改出現在所有現有例項中。

在上面的例子中,如果您執行 const a1 = new A(); const a2 = new A();,那麼 a1.doSomething 實際上將引用 Object.getPrototypeOf(a1).doSomething ——這與您定義的 A.prototype.doSomething 相同,即 Object.getPrototypeOf(a1).doSomething === Object.getPrototypeOf(a2).doSomething === A.prototype.doSomething

在編寫使用原型繼承模型的複雜程式碼之前,理解該模型至關重要。此外,請注意程式碼中原型鏈的長度,並在必要時將其拆分以避免可能的效能問題。此外,除非是為了相容更新的 JavaScript 功能,否則**絕不**應擴充套件原生原型。