继承和原型链

在编程中,继承是指将特性从父级传递给子级,以便新的代码可以重用并构建现有代码的功能。JavaScript 通过使用 objects 来实现继承。每个对象都有一个到另一个对象的内部链接,称为其原型。该原型对象有自己的原型,依此类推,直到到达以 null 作为原型的对象。根据定义,null 没有原型,并且充当该原型链中的最后一环。可以在运行时改变原型链的任何成员,甚至交换原型,因此像 静态调度 这样的概念在 JavaScript 中不存在。

¥In programming, inheritance refers to passing down characteristics from a parent to a child so that a new piece of code can reuse and build upon the features of an existing one. JavaScript implements inheritance by using objects. Each object has an internal link to another object called its prototype. That prototype object has a prototype of its own, and so on until an object is reached with null as its prototype. By definition, null has no prototype and acts as the final link in this prototype chain. It is possible to mutate any member of the prototype chain or even swap out the prototype at runtime, so concepts like static dispatching do not exist in JavaScript.

对于具有基于类的语言(如 Java 或 C++)经验的开发者来说,JavaScript 有点令人困惑,因为它是 动态 并且没有静态类型。虽然这种混乱通常被认为是 JavaScript 的弱点之一,但原型继承模型本身实际上比经典模型更强大。例如,在原型模型之上构建经典模型是相当简单的 - 这就是 classes 的实现方式。

¥JavaScript is a bit confusing for developers experienced in class-based languages (like Java or C++), as it is dynamic and does not have static types. While this confusion is often considered to be one of JavaScript's weaknesses, the prototypal inheritance model itself is, in fact, more powerful than the classic model. It is, for example, fairly trivial to build a classic model on top of a prototypal model — which is how classes are implemented.

尽管类现在被广泛采用并已成为 JavaScript 中的新范例,但类并没有带来新的继承模式。虽然类抽象了大部分原型机制,但了解原型如何在幕后工作仍然有用。

¥Although classes are now widely adopted and have become a new paradigm in JavaScript, classes do not bring a new inheritance pattern. While classes abstract most of the prototypal mechanism away, understanding how prototypes work under the hood is still useful.

与原型链的继承

¥Inheritance with the prototype chain

继承属性

¥Inheriting properties

JavaScript 对象具有动态 "bags" 属性(称为自身属性)。JavaScript 对象有一个到原型对象的链接。当尝试访问对象的属性时,不仅会在该对象上查找该属性,还会在该对象的原型、原型的原型等上查找,直到找到具有匹配名称的属性或结束 已达到原型链。

¥JavaScript objects are dynamic "bags" of properties (referred to as own properties). JavaScript objects have a link to a prototype object. When trying to access a property of an object, the property will not only be sought on the object but on the prototype of the object, the prototype of the prototype, and so on until either a property with a matching name is found or the end of the prototype chain is reached.

注意:遵循 ECMAScript 标准,符号 someObject.[[Prototype]] 用于指定 someObject 的原型。[[Prototype]] 内部插槽可分别使用 Object.getPrototypeOf()Object.setPrototypeOf() 功能进行访问和修改。这相当于 JavaScript 访问器 __proto__,它是非标准的,但实际上由许多 JavaScript 引擎实现。为了防止混淆并保持简洁,在我们的表示法中,我们将避免使用 obj.__proto__ 而是使用 obj.[[Prototype]]。这对应于 Object.getPrototypeOf(obj)

¥Note: Following the ECMAScript standard, the notation someObject.[[Prototype]] is used to designate the prototype of someObject. The [[Prototype]] internal slot can be accessed and modified with the Object.getPrototypeOf() and Object.setPrototypeOf() functions respectively. This is equivalent to the JavaScript accessor __proto__ which is non-standard but de-facto implemented by many JavaScript engines. To prevent confusion while keeping it succinct, in our notation we will avoid using obj.__proto__ but use obj.[[Prototype]] instead. This corresponds to Object.getPrototypeOf(obj).

它不应该与函数的 func.prototype 属性混淆,后者指定当用作构造函数时将 [[Prototype]] 分配给给定函数创建的对象的所有实例。我们将在 稍后的部分 中讨论构造函数的 prototype 属性。

¥It should not be confused with the func.prototype property of functions, which instead specifies the [[Prototype]] to be assigned to all instances of objects created by the given function when used as a constructor. We will discuss the prototype property of constructor functions in a later section.

指定对象的 [[Prototype]] 有多种方法,在 稍后的部分 中列出。现在,我们将使用 __proto__ 语法 进行说明。值得注意的是,{ __proto__: ... } 语法与 obj.__proto__ 访问器不同:前者是标准的并且不被弃用。

¥There are several ways to specify the [[Prototype]] of an object, which are listed in a later section. For now, we will use the __proto__ syntax for illustration. It's worth noting that the { __proto__: ... } syntax is different from the obj.__proto__ accessor: the former is standard and not deprecated.

在像 { a: 1, b: 2, __proto__: c } 这样的对象字面量中,值 c(必须是 null 或另一个对象)将成为该字面量所表示的对象的 [[Prototype]],而其他键(如 ab)将成为该对象自己的属性。这个语法读起来非常自然,因为 [[Prototype]] 只是对象的 "内部属性"。

¥In an object literal like { a: 1, b: 2, __proto__: c }, the value c (which has to be either null or another object) will become the [[Prototype]] of the object represented by the literal, while the other keys like a and b will become the own properties of the object. This syntax reads very naturally, since [[Prototype]] is just an "internal property" of the object.

以下是尝试访问属性时会发生的情况:

¥Here is what happens when trying to access a property:

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 拦截时。

¥Setting a property to an object creates an own property. The only exception to the getting and setting behavior rules is when it's intercepted by a getter or setter.

同样,你可以创建更长的原型链,并且将在所有原型链上寻找属性。

¥Similarly, you can create longer prototype chains, and a property will be sought on all of them.

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

继承 "methods"

¥Inheriting "methods"

JavaScript 没有基于类的语言定义形式的“methods”。在 JavaScript 中,任何函数都可以以属性的形式添加到对象中。继承函数的作用与任何其他属性一样,包括如上所示的属性遮蔽(在本例中,是方法重写的一种形式)。

¥JavaScript does not have "methods" in the form that class-based languages define them. In JavaScript, any function can be added to an object in the form of a property. An inherited function acts just as any other property, including property shadowing as shown above (in this case, a form of method overriding).

当执行继承的函数时,this 的值指向继承对象,而不是函数作为自己属性的原型对象。

¥When an inherited function is executed, the value of this points to the inheriting object, not to the prototype object where the function is an own property.

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

构造函数

¥Constructors

原型的强大之处在于,如果一组属性应该出现在每个实例上,我们就可以重用它们 - 尤其是方法。假设我们要创建一系列框,其中每个框都是一个对象,其中包含可以通过 getValue 函数访问的值。一个简单的实现是:

¥The power of prototypes is that we can reuse a set of properties if they should be present on every instance — especially for methods. Suppose we are to create a series of boxes, where each box is an object that contains a value which can be accessed through a getValue function. A naive implementation would be:

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

这是不合格的,因为每个实例都有自己的函数属性来执行相同的操作,这是多余且不必要的。相反,我们可以将 getValue 移动到所有框的 [[Prototype]]

¥This is subpar, because each instance has its own function property that does the same thing, which is redundant and unnecessary. Instead, we can move getValue to the [[Prototype]] of all boxes:

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 调用的函数。

¥This way, all boxes' getValue method will refer to the same function, lowering memory usage. However, manually binding the __proto__ for every object creation is still very inconvenient. This is when we would use a constructor function, which automatically sets the [[Prototype]] for every object manufactured. Constructors are functions called with 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。这允许从任何实例访问原始构造函数。

¥We say that new Box(1) is an instance created from the Box constructor function. Box.prototype is not much different from the boxPrototype object we created previously — it's just a plain object. Every instance created from a constructor function will automatically have the constructor's prototype property as its [[Prototype]] — that is, Object.getPrototypeOf(new Box()) === Box.prototype. Constructor.prototype by default has one own property: constructor, which references the constructor function itself — that is, Box.prototype.constructor === Box. This allows one to access the original constructor from any instance.

注意:如果从构造函数返回非基元,则该值将成为 new 表达式的结果。在这种情况下,[[Prototype]] 可能无法正确绑定 - 但实际上这种情况应该不会发生太多。

¥Note: If a non-primitive is returned from the constructor function, that value will become the result of the new expression. In this case the [[Prototype]] may not be correctly bound — but this should not happen much in practice.

上述构造函数在 classes 中可以重写为:

¥The above constructor function can be rewritten in classes as:

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

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

类是构造函数的语法糖,这意味着你仍然可以操作 Box.prototype 来更改所有实例的行为。但是,由于类被设计为对底层原型机制的抽象,因此我们将在本教程中使用更轻量级的构造函数语法来充分演示原型的工作原理。

¥Classes are syntax sugar over constructor functions, which means you can still manipulate Box.prototype to change the behavior of all instances. However, because classes are designed to be an abstraction over the underlying prototype mechanism, we will use the more-lightweight constructor function syntax for this tutorial to fully demonstrate how prototypes work.

因为 Box.prototype 与所有实例的 [[Prototype]] 引用相同的对象,所以我们可以通过改变 Box.prototype 来改变所有实例的行为。

¥Because Box.prototype references the same object as the [[Prototype]] of all instances, we can change the behavior of all instances by mutating 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 = ...) 不是一个好主意,原因有两个:

¥A corollary is, re-assigning Constructor.prototype (Constructor.prototype = ...) is a bad idea for two reasons:

  • 重新分配之前创建的实例的 [[Prototype]] 现在引用与重新分配之后创建的实例的 [[Prototype]] 不同的对象 - 改变一个的 [[Prototype]] 不再改变另一个。
  • 除非你手动重新设置 constructor 属性,否则无法再从 instance.constructor 跟踪构造函数,这可能会打破用户的期望。一些内置操作也会读取 constructor 属性,如果未设置,它们可能无法按预期工作。

Constructor.prototype 仅在构造实例时有用。它与 Constructor.[[Prototype]] 无关,Constructor.[[Prototype]] 是构造函数自己的原型,即 Function.prototype - 即 Object.getPrototypeOf(Constructor) === Function.prototype

¥Constructor.prototype is only useful when constructing instances. It has nothing to do with Constructor.[[Prototype]], which is the constructor function's own prototype, which is Function.prototype — that is, Object.getPrototypeOf(Constructor) === Function.prototype.

文字的隐式构造函数

¥Implicit constructors of literals

JavaScript 中的一些文字语法会创建隐式设置 [[Prototype]] 的实例。例如:

¥Some literal syntaxes in JavaScript create instances that implicitly set the [[Prototype]]. For example:

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

我们可以将它们 "de-sugar" 到构造函数形式中。

¥We can "de-sugar" them into their constructor form.

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

例如,"数组方法" 和 map() 一样只是在 Array.prototype 上定义的方法,这就是为什么它们在所有数组实例上自动可用。

¥For example, "array methods" like map() are simply methods defined on Array.prototype, which is why they are automatically available on all array instances.

警告:有一个曾经很流行的错误功能 - 扩展 Object.prototype 或其他内置原型之一。此错误功能的一个示例是定义 Array.prototype.myMethod = function () {...},然后在所有数组实例上使用 myMethod

¥Warning: There is one misfeature that used to be prevalent — extending Object.prototype or one of the other built-in prototypes. An example of this misfeature is, defining Array.prototype.myMethod = function () {...} and then using myMethod on all array instances.

这种错误功能称为猴子修补。进行猴子修补会带来向前兼容性的风险,因为如果该语言将来添加此方法但使用不同的签名,你的代码将会崩溃。它导致了像 SmooshGate 这样的事件,而且自从 JavaScript 试图实现 "不破坏网络" 以来,这对语言的进步来说可能是一个很大的麻烦。

¥This misfeature is called monkey patching. Doing monkey patching risks forward compatibility, because if the language adds this method in the future but with a different signature, your code will break. It has led to incidents like the SmooshGate, and can be a great nuisance for the language to advance since JavaScript tries to "not break the web".

The only 扩展内置原型的一个很好的理由是向后移植较新的 JavaScript 引擎(例如 Array.prototype.forEach)的功能。

¥good reason for extending a built-in prototype is to backport the features of newer JavaScript engines, like Array.prototype.forEach.

有趣的是,由于历史原因,一些内置构造函数的 prototype 属性本身就是实例。例如,Number.prototype 是数字 0,Array.prototype 是空数组,RegExp.prototype/(?:)/

¥It may be interesting to note that due to historical reasons, some built-in constructors' prototype property are instances themselves. For example, Number.prototype is a number 0, Array.prototype is an empty array, and RegExp.prototype is /(?:)/.

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 这样的现代构造函数来说,情况并非如此。

¥However, this is not the case for user-defined constructors, nor for modern constructors like Map.

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

建立更长的继承链

¥Building longer inheritance chains

Constructor.prototype 属性将按原样成为构造函数实例的 [[Prototype]] — 包括 Constructor.prototype 自己的 [[Prototype]]。默认情况下,Constructor.prototype 是一个普通对象,即 Object.getPrototypeOf(Constructor.prototype) === Object.prototype。唯一的例外是 Object.prototype 本身,其 [[Prototype]]null,即 Object.getPrototypeOf(Object.prototype) === null。因此,典型的构造函数将构建以下原型链:

¥The Constructor.prototype property will become the [[Prototype]] of the constructor's instances, as-is — including Constructor.prototype's own [[Prototype]]. By default, Constructor.prototype is a plain object — that is, Object.getPrototypeOf(Constructor.prototype) === Object.prototype. The only exception is Object.prototype itself, whose [[Prototype]] is null — that is, Object.getPrototypeOf(Object.prototype) === null. Therefore, a typical constructor will build the following prototype chain:

js
function Constructor() {}

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

为了构建更长的原型链,我们可以通过 Object.setPrototypeOf() 函数设置 [[Prototype]]Constructor.prototype

¥To build longer prototype chains, we can set the [[Prototype]] of Constructor.prototype via the Object.setPrototypeOf() function.

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 语法。

¥In class terms, this is equivalent to using the extends syntax.

js
class Base {}
class Derived extends Base {}

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

你可能还会看到一些使用 Object.create() 构建继承链的遗留代码。但是,由于这会重新分配 prototype 属性并删除 constructor 属性,因此可能更容易出错,而如果构造函数尚未创建任何实例,则性能提升可能并不明显。

¥You may also see some legacy code using Object.create() to build the inheritance chain. However, because this reassigns the prototype property and removes the constructor property, it can be more error-prone, while performance gains may not be apparent if the constructors haven't created any instances yet.

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);

检查原型:更深入的研究

¥Inspecting prototypes: a deeper dive

让我们更详细地看看幕后发生的事情。

¥Let's look at what happens behind the scenes in a bit more detail.

在 JavaScript 中,如上所述,函数可以拥有属性。所有函数都有一个名为 prototype 的特殊属性。请注意,下面的代码是独立的(可以安全地假设除了下面的代码之外,网页上没有其他 JavaScript)。为了获得最佳学习体验,强烈建议你打开控制台,导航到 "console" 选项卡,复制并粘贴以下 JavaScript 代码,然后按 Enter/Return 键运行它。(该控制台包含在大多数 Web 浏览器的开发者工具中。可提供有关 火狐开发者工具Chrome 开发工具Edge 开发工具 的更多信息。)

¥In JavaScript, as mentioned above, functions are able to have properties. All functions have a special property named prototype. Please note that the code below is free-standing (it is safe to assume there is no other JavaScript on the webpage other than the below code). For the best learning experience, it is highly recommended that you open a console, navigate to the "console" tab, copy-and-paste in the below JavaScript code, and run it by pressing the Enter/Return key. (The console is included in most web browser's Developer Tools. More information is available for Firefox Developer Tools, Chrome DevTools, and Edge DevTools.)

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 属性,如控制台所示。运行此代码后,控制台应该显示一个与此类似的对象。

¥As seen above, doSomething() has a default prototype property, as demonstrated by the console. After running this code, the console should have displayed an object that looks similar to this.

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

注意:Chrome 控制台使用 [[Prototype]] 来表示对象的原型,遵循规范的条款;Firefox 使用 <prototype>。为了保持一致性,我们将使用 [[Prototype]]

¥Note: The Chrome console uses [[Prototype]] to denote the object's prototype, following the spec's terms; Firefox uses <prototype>. For consistency we will use [[Prototype]].

我们可以给 doSomething() 的原型添加属性,如下所示。

¥We can add properties to the prototype of doSomething(), as shown below.

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

这导致:

¥This results in:

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

我们现在可以使用 new 运算符基于此原型创建 doSomething() 的实例。要使用 new 运算符,请正常调用该函数,但要在其前面加上 new 前缀。使用 new 运算符调用函数会返回一个作为该函数实例的对象。然后可以将属性添加到该对象上。

¥We can now use the new operator to create an instance of doSomething() based on this prototype. To use the new operator, call the function normally except prefix it with new. Calling a function with the new operator returns an object that is an instance of the function. Properties can then be added onto this object.

尝试以下代码:

¥Try the following code:

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);

这会产生类似于以下内容的输出:

¥This results in an output similar to the following:

{
  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 是否具有该属性。

¥As seen above, the [[Prototype]] of doSomeInstancing is doSomething.prototype. But, what does this do? When you access a property of doSomeInstancing, the runtime first looks to see if doSomeInstancing has that property.

如果 doSomeInstancing 没有该属性,则运行时会在 doSomeInstancing.[[Prototype]](也称为 doSomething.prototype)中查找该属性。如果 doSomeInstancing.[[Prototype]] 具有正在查找的属性,则使用 doSomeInstancing.[[Prototype]] 上的该属性。

¥If doSomeInstancing does not have the property, then the runtime looks for the property in doSomeInstancing.[[Prototype]] (a.k.a. doSomething.prototype). If doSomeInstancing.[[Prototype]] has the property being looked for, then that property on doSomeInstancing.[[Prototype]] is used.

否则,如果 doSomeInstancing.[[Prototype]] 不具有该属性,则检查 doSomeInstancing.[[Prototype]].[[Prototype]] 是否具有该属性。默认情况下,任何函数的 prototype 属性的 [[Prototype]] 都是 Object.prototype。因此,然后查找 doSomeInstancing.[[Prototype]].[[Prototype]](又名 doSomething.prototype.[[Prototype]](又名 Object.prototype))以查找正在搜索的属性。

¥Otherwise, if doSomeInstancing.[[Prototype]] does not have the property, then doSomeInstancing.[[Prototype]].[[Prototype]] is checked for the property. By default, the [[Prototype]] of any function's prototype property is Object.prototype. So, doSomeInstancing.[[Prototype]].[[Prototype]] (a.k.a. doSomething.prototype.[[Prototype]] (a.k.a. Object.prototype)) is then looked through for the property being searched for.

如果在 doSomeInstancing.[[Prototype]].[[Prototype]] 中没有找到该属性,则查找 doSomeInstancing.[[Prototype]].[[Prototype]].[[Prototype]]。然而,有一个问题:doSomeInstancing.[[Prototype]].[[Prototype]].[[Prototype]] 不存在,因为 Object.prototype.[[Prototype]]null。然后,只有到那时,在查看完 [[Prototype]] 的整个原型链之后,运行时才会断言该属性不存在,并得出该属性的值为 undefined 的结论。

¥If the property is not found in doSomeInstancing.[[Prototype]].[[Prototype]], then doSomeInstancing.[[Prototype]].[[Prototype]].[[Prototype]] is looked through. However, there is a problem: doSomeInstancing.[[Prototype]].[[Prototype]].[[Prototype]] does not exist, because Object.prototype.[[Prototype]] is null. Then, and only then, after the entire prototype chain of [[Prototype]]'s is looked through, the runtime asserts that the property does not exist and conclude that the value at the property is undefined.

让我们尝试在控制台中输入更多代码:

¥Let's try entering some more code into the console:

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);

结果如下:

¥This results in the following:

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

创建和改变原型链的不同方法

¥Different ways of creating and mutating prototype chains

我们遇到过许多创建对象并更改其原型链的方法。我们将系统地总结不同的方法,比较每种方法的优缺点。

¥We have encountered many ways to create objects and change their prototype chains. We will systematically summarize the different ways, comparing each approach's pros and cons.

使用语法结构创建的对象

¥Objects created with syntax constructs

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", "whadup", "?"];
// 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
Pros and cons of using the __proto__ key in 对象初始值设定项
Pro(s) 所有现代引擎均受支持。将 __proto__ 键指向非对象的东西只会默默地失败,而不会引发异常。与 Object.prototype.proto setter 相反,对象字面量初始化器中的 __proto__ 是标准化和优化的,甚至可以比 Object.create 性能更高。在创建对象时声明自己的额外属性比 Object.create 更符合人机工程学。
Con(s) IE10 及以下版本不支持。对于不了解差异的人来说,可能会与 Object.prototype.proto 访问器混淆。

带有构造函数

¥With constructor functions

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.
Pros and cons of using constructor functions
Pro(s) 所有引擎均受支持 — 一直到 IE 5.5。而且,它非常快、非常标准并且非常易于 JIT 优化。
Con(s)
  • In order to use this method, the function in question must be initialized. During this initialization, the constructor may store unique information that must be generated per-object. This unique information would only be generated once, potentially leading to problems.
  • The initialization of the constructor may put unwanted methods onto the object.

这两者在实践中通常都不是问题。

使用 Object.create()

¥With Object.create()

调用 Object.create() 创建一个新对象。该对象的 [[Prototype]] 是该函数的第一个参数:

¥Calling Object.create() creates a new object. The [[Prototype]] of this object is the first argument of the function:

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
Pros and cons of Object.create
Pro(s) 所有现代引擎均受支持。允许在创建时直接设置对象的 [[Prototype]],这允许运行时进一步优化对象。还允许使用 Object.create(null) 创建没有原型的对象。
Con(s) IE8 及以下版本不支持。但是,由于 Microsoft 已停止对运行 IE8 及更低版本的系统的扩展支持,因此对于大多数应用来说,这不应该是一个问题。此外,如果使用第二个参数,缓慢的对象初始化可能会成为性能黑洞,因为每个对象描述符属性都有自己单独的描述符对象。当处理对象形式的数十万个对象描述符时,延迟时间可能会成为一个严重的问题。

有课

¥With classes

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
Pros and cons of classes
Pro(s) 所有现代引擎均受支持。非常高的可读性和可维护性。 私有属性 是原型继承中不可替代的功能。
Con(s) 类,尤其是具有私有属性的类,其优化程度低于传统类(尽管引擎实现者正在努力改进这一点)。在较旧的环境中不受支持,并且通常需要转译器才能在生产中使用类。

使用 Object.setPrototypeOf()

¥With Object.setPrototypeOf()

虽然上述所有方法都会在对象创建时设置原型链,但 Object.setPrototypeOf() 允许更改现有对象的 [[Prototype]] 内部属性。

¥While all methods above will set the prototype chain at object creation time, Object.setPrototypeOf() allows mutating the [[Prototype]] internal property of an existing object.

js
const obj = { a: 1 };
const anotherObj = { b: 2 };
Object.setPrototypeOf(obj, anotherObj);
// obj ---> anotherObj ---> Object.prototype ---> null
Pros and cons of Object.setPrototypeOf
Pro(s) 所有现代引擎均受支持。允许动态操作对象的原型,甚至可以在使用 Object.create(null) 创建的无原型对象上强制使用原型。
Con(s) 表现不佳。如果可以在对象创建时设置原型,则应避免这样做。许多引擎优化原型,在调用实例时尝试提前猜测方法在内存中的位置;但动态设置原型会破坏所有这些优化。它可能会导致某些引擎重新编译你的代码以进行去优化,以使其按照规范运行。IE8 及以下版本不支持。

使用 proto 访问器

¥With the __proto__ accessor

所有对象都继承 Object.prototype.__proto__ setter,它可用于设置现有对象的 [[Prototype]](如果对象上未覆盖 __proto__ 键)。

¥All objects inherit the Object.prototype.__proto__ setter, which can be used to set the [[Prototype]] of an existing object (if the __proto__ key is not overridden on the object).

警告:Object.prototype.__proto__ 访问器是非标准且已弃用。你几乎应该始终使用 Object.setPrototypeOf

¥Warning: Object.prototype.__proto__ accessors are non-standard and deprecated. You should almost always use Object.setPrototypeOf instead.

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);
Pros and cons of setting the __proto__ property
Pro(s) 所有现代引擎均受支持。将 proto 设置为非对象只会失败。它不会抛出异常。
Con(s) 性能不佳且已弃用。很多引擎对原型进行了优化,在调用实例时尝试提前猜测方法在内存中的位置;但是动态设置原型会破坏所有这些优化,甚至可以强制某些引擎重新编译以对代码进行去优化,以使其按照规范运行。IE10 及以下版本不支持。proto setter 是规范可选的,因此它可能不适用于所有平台。你几乎应该始终使用 Object.setPrototypeOf

性能

¥Performance

原型链上较高属性的查找时间可能会对性能产生负面影响,这在性能至关重要的代码中可能很重要。此外,尝试访问不存在的属性将始终遍历完整的原型链。

¥The lookup time for properties that are high up on the prototype chain can have a negative impact on the performance, and this may be significant in the code where performance is critical. Additionally, trying to access nonexistent properties will always traverse the full prototype chain.

此外,当迭代对象的属性时,原型链上的每个可枚举属性都将被枚举。要检查对象是否具有在其自身上定义的属性,而不是在其原型链上的某个位置定义的属性,有必要使用 hasOwnPropertyObject.hasOwn 方法。所有对象(除了那些 null[[Prototype]] 的对象)都从 Object.prototype 继承 hasOwnProperty - 除非它在原型链中被进一步覆盖。为了给大家举个具体的例子,我们以上面的图例代码来说明一下:

¥Also, when iterating over the properties of an object, every enumerable property that is on the prototype chain will be enumerated. To check whether an object has a property defined on itself and not somewhere on its prototype chain, it is necessary to use the hasOwnProperty or Object.hasOwn methods. All objects, except those with null as [[Prototype]], inherit hasOwnProperty from Object.prototype — unless it has been overridden further down the prototype chain. To give you a concrete example, let's take the above graph example code to illustrate it:

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

¥Note: It is not enough to check whether a property is undefined. The property might very well exist, but its value just happens to be set to undefined.

结论

¥Conclusion

对于来自 Java 或 C++ 的开发者来说,JavaScript 可能有点令人困惑,因为它都是动态的、都是运行时的,而且根本没有静态类型。一切要么是对象(实例),要么是函数(构造函数),甚至函数本身也是 Function 构造函数的实例。即使是 "classes" as 语法构造也只是运行时的构造函数。

¥JavaScript may be a bit confusing for developers coming from Java or C++, as it's all dynamic, all runtime, and it has no static types at all. Everything is either an object (instance) or a function (constructor), and even functions themselves are instances of the Function constructor. Even the "classes" as syntax constructs are just constructor functions at runtime.

JavaScript 中的所有构造函数都有一个名为 prototype 的特殊属性,它与 new 运算符一起使用。对原型对象的引用被复制到新实例的内部 [[Prototype]] 属性。例如,当你执行 const a1 = new A() 时,JavaScript(在内存中创建对象之后、运行定义了 this 的函数 A() 之前)会设置 a1.[[Prototype]] = A.prototype。然后,当你访问实例的属性时,JavaScript 首先检查它们是否直接存在于该对象上,如果不存在,则会在 [[Prototype]] 中查找。递归地查看 [[Prototype]],即 a1.doSomethingObject.getPrototypeOf(a1).doSomethingObject.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething 等,直到找到它或 Object.getPrototypeOf 返回 null。这意味着 prototype 上定义的所有属性均由所有实例有效共享,你甚至可以稍后更改 prototype 的部分内容并使更改出现在所有现有实例中。

¥All constructor functions in JavaScript have a special property called prototype, which works with the new operator. The reference to the prototype object is copied to the internal [[Prototype]] property of the new instance. For example, when you do const a1 = new A(), JavaScript (after creating the object in memory and before running function A() with this defined to it) sets a1.[[Prototype]] = A.prototype. When you then access properties of the instance, JavaScript first checks whether they exist on that object directly, and if not, it looks in [[Prototype]]. [[Prototype]] is looked at recursively, i.e. a1.doSomething, Object.getPrototypeOf(a1).doSomething, Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething etc., until it's found or Object.getPrototypeOf returns null. This means that all properties defined on prototype are effectively shared by all instances, and you can even later change parts of prototype and have the changes appear in all existing instances.

在上面的示例中,如果你执行 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

¥If, in the example above, you do const a1 = new A(); const a2 = new A();, then a1.doSomething would actually refer to Object.getPrototypeOf(a1).doSomething — which is the same as the A.prototype.doSomething you defined, i.e. Object.getPrototypeOf(a1).doSomething === Object.getPrototypeOf(a2).doSomething === A.prototype.doSomething.

在编写使用原型继承模型的复杂代码之前,了解原型继承模型至关重要。另外,请注意代码中原型链的长度,并在必要时将其分解,以避免可能的性能问题。此外,除非是为了与新的 JavaScript 功能兼容,否则永远不应该扩展原生原型。

¥It is essential to understand the prototypal inheritance model before writing complex code that makes use of it. Also, be aware of the length of the prototype chains in your code and break them up if necessary to avoid possible performance problems. Further, the native prototypes should never be extended unless it is for the sake of compatibility with newer JavaScript features.