继承和原型链

在编程中,继承指的是将特性从父代传递给子代,以便新代码可以重用和构建现有代码的特性。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.prototypeConstructor.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 引擎的功能,例如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", "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

对象初始化器中使用__proto__键时,将__proto__键指向非对象只会静默失败,而不会抛出异常。与Object.prototype.__proto__设置器相反,对象字面量初始化器中的__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__设置器,可用于设置现有对象的[[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(在内存中创建对象并在使用定义为它的this运行函数A()之前)设置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功能兼容,否则**绝不**应该扩展原生原型。