继承与原型链

对于使用过基于类的语言 (如 Java 或 C++) 的开发者们来说,JavaScript
实在是有些令人困惑 —— JavaScript
是动态的,本身不提供一个 class 的实现。即便是在 ES2015/ES6
中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。

当谈到继承时,JavaScript
只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为
proto
)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript
中的对象都是位于原型链顶端的 [Object]{.ul} 的实例。

尽管这种原型继承通常被认为是 JavaScript
的弱点之一,但是原型继承模型本身实际上比经典模型更强大。例如,在原型模型的基础上构建经典模型相当简单。

[基于原型链的继承]{.ul}

[继承属性]{.ul}

JavaScript 对象是动态的属性”包”(指其自己的属性)。JavaScript
对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

遵循ECMAScript标准,someObject.[[Prototype]] 符号是用于指向 someObject 的原型。从
ECMAScript 6
开始,[[Prototype]] 可以通过 [Object.getPrototypeOf()]{.ul} 和 [Object.setPrototypeOf()]{.ul} 访问器来访问。这个等同于
JavaScript 的非标准但许多浏览器实现的属性 __proto__。

但它不应该与构造函数 func 的 prototype 属性相混淆。被构造函数创建的实例对象的 [[Prototype]] 指向 func 的 prototype 属性。**Object.prototype **属性表示 [Object]{.ul} 的原型对象。

这里演示当尝试访问属性时会发生什么:

// 让我们从一个函数里创建一个对象o,它自身拥有属性a和b的:

let f = function () {

this.a = 1;

this.b = 2;

}

/* 这么写也一样

function f() {

this.a = 1;

this.b = 2;

}

*/

let o = new f(); // {a: 1, b: 2}

// 在f函数的原型上定义属性

f.prototype.b = 3;

f.prototype.c = 4;

// 不要在 f 函数的原型上直接定义 f.prototype =
{b:3,c:4};这样会直接打破原型链

// o.[[Prototype]] 有属性 b 和 c

// (其实就是 o.proto 或者 o.constructor.prototype)

// o.[[Prototype]].[[Prototype]] 是 Object.prototype.

// 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null

// 这就是原型链的末尾,即 null,

// 根据定义,null 就是没有 [[Prototype]]。

// 综上,整个原型链如下:

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

console.log(o.a); // 1

// a是o的自身属性吗?是的,该属性的值为 1

console.log(o.b); // 2

// b是o的自身属性吗?是的,该属性的值为 2

// 原型上也有一个’b’属性,但是它不会被访问到。

// 这种情况被称为”属性遮蔽 (property shadowing)”

console.log(o.c); // 4

// c是o的自身属性吗?不是,那看看它的原型上有没有

// c是o.[[Prototype]]的属性吗?是的,该属性的值为 4

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

// d 是 o 的自身属性吗?不是,那看看它的原型上有没有

// d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有

// o.[[Prototype]].[[Prototype]] 为 null,停止搜索

// 找不到 d 属性,返回 undefined

代码来源链接:[https://repl.it/@khaled_hossain_code/prototype]{.ul}

给对象设置属性会创建自有属性。获取和设置属性的唯一限制是内置 [getter 或
setter]{.ul}
 的属性。

[继承方法]{.ul}

JavaScript 并没有其他基于类的语言所定义的”方法”。在 JavaScript
里,任何函数都可以添加到对象上作为对象的属性。函数的继承与其他的属性继承没有差别,包括上面的”属性遮蔽”(这种情况相当于其他语言的方法重写)。

当继承的函数被调用时,[this]{.ul} 指向的是当前继承的对象,而不是继承的函数所在的原型对象。

var o = {

a: 2,

m: function(){

return this.a + 1;

}

};

console.log(o.m()); // 3

// 当调用 o.m 时,’this’ 指向了 o.

var p = Object.create(o);

// p是一个继承自 o 的对象

p.a = 4; // 创建 p 的自身属性 ‘a’

console.log(p.m()); // 5

// 调用 p.m 时,’this’ 指向了 p

// 又因为 p 继承了 o 的 m 函数

// 所以,此时的 ‘this.a’ 即 p.a,就是 p 的自身属性 ‘a’

[使用不同的方法来创建对象和生成原型链]{.ul}

[使用语法结构创建的对象]{.ul}

var o = {a: 1};

// o 这个对象继承了 Object.prototype 上面的所有属性

// o 自身没有名为 hasOwnProperty 的属性

// hasOwnProperty 是 Object.prototype 的属性

// 因此 o 继承了 Object.prototype 的 hasOwnProperty

// Object.prototype 的原型为 null

// 原型链如下:

// o —> Object.prototype —> null

var a = [“yo”, “whadup”, “?”];

// 数组都继承于 Array.prototype

// (Array.prototype 中包含 indexOf, forEach 等方法)

// 原型链如下:

// a —> Array.prototype —> Object.prototype —> null

function f(){

return 2;

}

// 函数都继承于 Function.prototype

// (Function.prototype 中包含 call, bind等方法)

// 原型链如下:

// f —> Function.prototype —> Object.prototype —> null

[使用构造器创建的对象]{.ul}

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 [new
操作符]{.ul}
 来作用这个函数时,它就可以被称为构造方法(构造函数)。

function Graph() {

this.vertices = [];

this.edges = [];

}

Graph.prototype = {

addVertex: function(v){

this.vertices.push(v);

}

};

var g = new Graph();

// g 是生成的对象,他的自身属性有 ‘vertices’ 和 ‘edges’。

// 在 g 被实例化时,g.[[Prototype]] 指向了 Graph.prototype。

[使用 ]{.ul}Object.create[ 创建的对象]{.ul}

ECMAScript 5
中引入了一个新方法:[Object.create()]{.ul}。可以调用这个方法来创建一个新对象。新对象的原型就是调用
create 方法时传入的第一个参数:

var a = {a: 1};

// a —> Object.prototype —> null

var b = Object.create(a);

// b —> a —> Object.prototype —> null

console.log(b.a); // 1 (继承而来)

var c = Object.create(b);

// c —> b —> a —> Object.prototype —> null

var d = Object.create(null);

// d —> null

console.log(d.hasOwnProperty); // undefined,
因为d没有继承Object.prototype

[使用 ]{.ul}class[ 关键字创建的对象]{.ul}

ECMAScript6
引入了一套新的关键字用来实现 [class]{.ul}。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript
仍然基于原型。这些新的关键字包括 [class]{.ul}[constructor]{.ul}[static]{.ul}[extends]{.ul} 和 [super]{.ul}

“use strict”;

class Polygon {

constructor(height, width) {

this.height = height;

this.width = width;

}

}

class Square extends Polygon {

constructor(sideLength) {

super(sideLength, sideLength);

}

get area() {

return this.height * this.width;

}

set sideLength(newLength) {

this.height = newLength;

this.width = newLength;

}

}

var square = new Square(2);

[性能]{.ul}

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。

遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 [hasOwnProperty
(en-US)]{.ul} 方法。下面给出一个具体的例子来说明它:

console.log(g.hasOwnProperty(‘vertices’));

// true

console.log(g.hasOwnProperty(‘nope’));

// false

console.log(g.hasOwnProperty(‘addVertex’));

// false

console.log(g.proto.hasOwnProperty(‘addVertex’));

// true

[hasOwnProperty
(en-US)]{.ul}
 是
JavaScript
中唯一一个处理属性并且不会遍历原型链的方法。(译者注:原文如此。另一种这样的方法:[Object.keys()]{.ul}

注意:检查属性是否为 [undefined]{.ul} 是不能够检查其是否存在的。该属性可能已存在,但其值恰好被设置成了 undefined。

[错误实践:扩展原生对象的原型]{.ul}

经常使用的一个错误实践是扩展 Object.prototype 或其他内置原型。

这种技术被称为猴子补丁并且会破坏封装。尽管一些流行的框架(如
Prototype.js)在使用该技术,但仍然没有足够好的理由使用附加的非标准方法来混入内置原型。

扩展内置原型的唯一理由是支持 JavaScript
引擎的新特性,如 Array.forEach。

[总结:4
个用于拓展原型链的方法]{.ul}

下面列举四种用于拓展原型链的方法,以及他们的优势和缺陷。下列四个例子都创建了完全相同的 inst 对象(所以在控制台上的输出也是一致的),为了举例,唯一的区别是他们的创建方法不同。

+—————-+———————-+——————–+——–+
| 名称 | 例子 | 优势 | 缺陷 |
+================+======================+====================+========+
| New- | function foo(){} | 支持目前以及所有 | 为使用 |
| initialization | | 可想象到的浏览器( | 此方法 |
| | foo.prototype = { | IE5.5都可以使用)。 | ,必须 |
| | | 这种方法非常快, | 对相关 |
| | foo_prop: “foo | 非常符合标准,并且 | 函数初 |
| | val” | 充分利用JIT优化。 | 始化。 |
| | | | 在初始 |
| | }; | | 化过程 |
| | | | 中,构 |
| | function bar(){} | | 造函数 |
| | | | 可以存 |
| | var proto = new foo; | | 储每个 |
| | | | 对象必 |
| | proto.bar_prop = | | 须生成 |
| | “bar val”; | | 的唯一 |
| | | | 信息。 |
| | bar.prototype = | | 但是, |
| | proto; | | 这种唯 |
| | | | 一信息 |
| | var inst = new bar; | | 只生成 |
| | | | 一次, |
| | console | | 可能会 |
| | .log(inst.foo_prop); | | 带来潜 |
| | | | 在的问 |
| | console | | 题。此 |
| | .log(inst.bar_prop); | | 外,构 |
| | | | 造函数 |
| | Copy to Clipboard | | 的初始 |
| | | | 化,可 |
| | | | 能会将 |
| | | | 不需要 |
| | | | 的方法 |
| | | | 放在对 |
| | | | 象上。 |
| | | | 然而, |
| | | | 如果你 |
| | | | 只在自 |
| | | | 己的代 |
| | | | 码中使 |
| | | | 用,你 |
| | | | 也清楚 |
| | | | (或有 |
| | | | 通过注 |
| | | | 释等写 |
| | | | 明)各 |
| | | | 段代码 |
| | | | 在做什 |
| | | | 么,这 |
| | | | 些在大 |
| | | | 体上都 |
| | | | 不是问 |
| | | | 题(事 |
| | | | 实上, |
| | | | 通常是 |
| | | | 有益处 |
| | | | 的)。 |
+—————-+———————-+——————–+——–+
| Object.create | function foo(){} | 支持当前 | 不支持 |
| | | 所有非微软版本或者 | IE8 |
| | foo.prototype = { | IE9 | 以下 |
| | | 以上 | 的版本 |
| | foo_prop: “foo | 版本的浏览器。允许 | 。然而 |
| | val” | 一次性地直接设置  | ,随着 |
| | | proto 属性 | 微软不 |
| | }; | ,以便浏览器能更好 | 再对系 |
| | | 地优化对象。同时允 | 统中运 |
| | function bar(){} | 许通过 Object.cre | 行的旧 |
| | | ate(null) 来创建一 | 版本浏 |
| | var proto = | 个没有原型的对象。 | 览器提 |
| | Object.create( | | 供支持 |
| | | | ,这将 |
| | foo.prototype | | 不是在 |
| | | | 大多数 |
| | ); | | 应用中 |
| | | | 的主要 |
| | proto.bar_prop = | | 问题。 |
| | “bar val”; | | 另 |
| | | | 外,这 |
| | bar.prototype = | | 个慢对 |
| | proto; | | 象初始 |
| | | | 化在使 |
| | var inst = new bar; | | 用第二 |
| | | | 个参数 |
| | console | | 的时候 |
| | .log(inst.foo_prop); | | 有可能 |
| | | | 成为一 |
| | console | | 个性能 |
| | .log(inst.bar_prop); | | 黑洞, |
| | | | 因为每 |
| | | | 个对象 |
| | | | 的描述 |
| | | | 符属性 |
| | | | 都有自 |
| | | | 己的描 |
| | | | 述对象 |
| | | | 。当以 |
| | | | 对象的 |
| | | | 格式处 |
| | | | 理成百 |
| | | | 上千的 |
| | | | 对象描 |
| | | | 述的时 |
| | | | 候,可 |
| | | | 能会造 |
| | | | 成严重 |
| | | | 的性能 |
| | | | 问题。 |
+—————-+———————-+——————–+——–+
| Object. | function foo(){} | 支持所有现 | 这个 |
| setPrototypeOf | | 代浏览器和微软IE9+ | 方式表 |
| | foo.prototype = { | 浏览器。允许动态操 | 现并不 |
| | | 作对象的原型,甚至 | 好,应 |
| | foo_prop: “foo | 能强制给通过 Obje | 该被弃 |
| | val” | ct.create(null) 创 | 用。如 |
| | | 建出来的没有原型的 | 果你在 |
| | }; | 对象添加一个原型。 | 生产环 |
| | | | 境中使 |
| | function bar(){} | | 用这个 |
| | | | 方法, |
| | var proto = { | | 那么快 |
| | | | 速运行 |
| | bar_prop: “bar | | Java |
| | val” | | script |
| | | | 就是不 |
| | }; | | 可能的 |
| | | | ,因为 |
| | Ob | | 许多浏 |
| | ject.setPrototypeOf( | | 览器优 |
| | | | 化了原 |
| | proto, foo.prototype | | 型,尝 |
| | | | 试在调 |
| | ); | | 用实例 |
| | | | 之前猜 |
| | bar.prototype = | | 测方法 |
| | proto; | | 在内存 |
| | | | 中的位 |
| | var inst = new bar; | | 置,但 |
| | | | 是动态 |
| | console | | 设置原 |
| | .log(inst.foo_prop); | | 型干扰 |
| | | | 了所有 |
| | console | | 的优化 |
| | .log(inst.bar_prop); | | ,甚至 |
| | | | 可能使 |
| | | | 浏览器 |
| | | | 为了运 |
| | | | 行成功 |
| | | | ,使用 |
| | | | 完全未 |
| | | | 经优化 |
| | | | 的代码 |
| | | | 进行重 |
| | | | 编译。 |
| | | | 不支持 |
| | | | IE8 |
| | | | 及 |
| | | | 以下的 |
| | | | 浏览器 |
| | | | 版本。 |
+—————-+———————-+——————–+——–+
| proto | function foo(){} | 支持所有 | 应该完 |
| | | 现代非微软版本以及 | 全将其 |
| | foo.prototype = { | IE11 | 抛弃因 |
| | | 以上版本 | 为这个 |
| | foo_prop: “foo | 的浏览器。将 __ | 行为完 |
| | val” | proto__ 设置为非 | 全不具 |
| | | 对象的值会静默失败 | 备性能 |
| | }; | ,并不会抛出错误。 | 可言。 |
| | | | 如 |
| | function bar(){} | | 果你在 |
| | | | 生产环 |
| | var proto = { | | 境中使 |
| | | | 用这个 |
| | bar_prop: “bar | | 方法, |
| | val”, | | 那么快 |
| | | | 速运行 |
| | proto: | | Java |
| | foo.prototype | | script |
| | | | 就是不 |
| | }; | | 可能的 |
| | | | ,因为 |
| | bar.prototype = | | 许多浏 |
| | proto; | | 览器优 |
| | | | 化了原 |
| | var inst = new bar; | | 型,尝 |
| | | | 试在调 |
| | console | | 用实例 |
| | .log(inst.foo_prop); | | 之前猜 |
| | | | 测方法 |
| | console | | 在内存 |
| | .log(inst.bar_prop); | | 中的位 |
| | | | 置,但 |
| | | | 是动态 |
| | | | 设置原 |
| | | | 型干扰 |
| | | | 了所有 |
| | | | 的优化 |
| | | | ,甚至 |
| | | | 可能使 |
| | | | 浏览器 |
| | | | 为了运 |
| | | | 行成功 |
| | | | ,使用 |
| | | | 完全未 |
| | | | 经优化 |
| | | | 的代码 |
| | | | 进行重 |
| | | | 编译。 |
| | | | 不支持 |
| | | | IE10 |
| | | | 及 |
| | | | 以下的 |
| | | | 浏览器 |
| | | | 版本。 |
+—————-+———————-+——————–+——–+

prototype[ 和 ]{.ul}Object.getPrototypeOf

对于从 Java 或 C++ 转过来的开发人员来说,JavaScript
会有点让人困惑,因为它完全是动态的,都是运行时,而且不存在类(class)。所有的都是实例(对象)。即使我们模拟出的
“类”,也只是一个函数对象。

你可能已经注意到我们的 function A
有一个叫做 prototype 的特殊属性。该特殊属性可与 JavaScript
的 new 操作符一起使用。对原型对象的引用被复制到新实例的内部 [[Prototype]] 属性。例如,当执行 var
a1 = new
A(); 时,JavaScript(在内存中创建对象之后,和在运行函数 A() 把 this 指向对象之前)设置 a1.[[Prototype]]
= A.prototype;。然后当您访问实例的属性时,JavaScript
首先会检查它们是否直接存在于该对象上,如果不存在,则会 [[Prototype]] 中查找。这意味着你在 prototype 中定义的所有内容都可以由所有实例有效地共享,你甚至可以稍后更改部分 prototype,并在所有现有实例中显示更改(如果有必要的话)。

像上面的例子中,如果你执行 var a1 = new A(); var a2 = new
A(); 那么 a1.doSomething 事实上会指向 Object.getPrototypeOf(a1).doSomething,它就是你在 A.prototype.doSomething 中定义的内容。也就是说:Object.getPrototypeOf(a1).doSomething
== Object.getPrototypeOf(a2).doSomething ==
A.prototype.doSomething(补充:实际上,执行 a1.doSomething() 相当于执行 Object.getPrototypeOf(a1).doSomething.call(a1)==A.prototype.doSomething.call(a1))

简而言之, prototype 是用于类的,而 Object.getPrototypeOf() 是用于实例的(instances),两者功能一致。

[[Prototype]] 看起来就像递归引用,
如 a1.doSomething、Object.getPrototypeOf(a1).doSomething、Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething 等等等,
直到它被找到或 Object.getPrototypeOf 返回 null。

因此,当你执行:

var o = new Foo();

JavaScript 实际上执行的是:

var o = new Object();

o.proto = Foo.prototype;

Foo.call(o);

(或者类似上面这样的),然后,当你执行:

o.someProp;

它检查 o
是否具有 someProp 属性。如果没有,它会查找 Object.getPrototypeOf(o).someProp,如果仍旧没有,它会继续查找 Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp。

[结论]{.ul}

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

译者注:在英文原版中,以下内容已被移除。保留仅作参考。

[示例]{.ul}

B 继承自 A:

function A(a){

this.varA = a;

}

// 以上函数 A 的定义中,既然 A.prototype.varA 总是会被 this.varA 遮蔽,

// 那么将 varA 加入到原型(prototype)中的目的是什么?

A.prototype = {

varA : null,

/*

既然它没有任何作用,干嘛不将 varA 从原型(prototype)去掉 ?

也许作为一种在隐藏类中优化分配空间的考虑 ?

https://developers.google.com/speed/articles/optimizing-javascript

如果varA并不是在每个实例中都被初始化,那这样做将是有效果的。

*/

doSomething : function(){

// …

}

}

function B(a, b){

A.call(this, a);

this.varB = b;

}

B.prototype = Object.create(A.prototype, {

varB : {

value: null,

enumerable: true,

configurable: true,

writable: true

},

doSomething : {

value: function(){ // override

A.prototype.doSomething.apply(this, arguments);

// call super

// …

},

enumerable: true,

configurable: true,

writable: true

}

});

B.prototype.constructor = B;

var b = new B();

b.doSomething();

最重要的部分是:

  • 类型被定义在 .prototype 中

  • 用 Object.create() 来继承