实际上,我们已经看到了一个常被称为“原型继承”的机制如何工作: 可以“继承自” Foo.prototype,并因此可以访问 myName() 函数。但是我们传统的想法认为“继承”是两个“类”间的关系,而非“类”与“实例”的关系。

    回想之前这幅图,它不仅展示了从对象(也就是“实例”)a1 到对象 Foo.prototype 的委托,而且从 Bar.prototypeFoo.prototype,这酷似类继承的亲自概念。酷似,除了方向,箭头表示的是委托链接,而不是拷贝操作。

    这里是一段典型的创建这样的链接的“原型风格”代码:

    注意: 要想知道为什么上面代码中的 this 指向 a,参见第二章。

    重要的部分是 Bar.prototype = Object.create( Foo.prototype )Object.create(..) 凭空 创建 了一个“新”对象,并将这个新对象内部的 [[Prototype]] 链接到你指定的对象上(在这里是 Foo.prototype)。

    换句话说,这一行的意思是:“做一个 新的 链接到‘Foo 点儿 prototype’的‘Bar 点儿 prototype ’对象”。

    function Bar() { .. } 被声明时,就像其他函数一样,拥有一个链到默认对象的 .prototype 链接。但是 那个 对象没有链到我们希望的 Foo.prototype。所以,我们创建了一个 对象,链到我们希望的地方,并将原来的错误链接的对象扔掉。

    注意: 这里一个常见的误解/困惑是,下面两种方法 能工作,但是他们不会如你期望的那样工作:

    1. // 不会如你期望的那样工作!
    2. Bar.prototype = Foo.prototype;
    3. // 会如你期望的那样工作
    4. // 但会带有你可能不想要的副作用 :(
    5. Bar.prototype = new Foo();

    Bar.prototype = Foo.prototype 不会创建新对象让 Bar.prototype 链接。它只是让 Bar.prototype 成为 Foo.prototype 的另一个引用,将 Bar 直接链到 Foo 链着的 同一个对象Foo.prototype。这意味着当你开始赋值时,比如 Bar.prototype.myLabel = ...,你修改的 不是一个分离的对象 而是那个被分享的 Foo.prototype 对象本身,它将影响到所有链接到 Foo.prototype 的对象。这几乎可以确定不是你想要的。如果这正是你想要的,那么你根本就不需要 Bar,你应当仅使用 Foo 来使你的代码更简单。

    Bar.prototype = new Foo() 确实 创建了一个新的对象,这个新对象也的确链接到了我们希望的 Foo.prototype。但是,它是用 Foo(..) “构造器调用”来这样做的。如果这个函数有任何副作用(比如 logging,改变状态,注册其他对象,this 添加数据属性,等等),这些副作用就会在链接时发生(而且很可能是对错误的对象!),而不是像可能希望的那样,仅最终在 Bar() 的“后裔”被创建时发生。

    于是,我们剩下的选择就是使用 Object.create(..) 来制造一个新对象,这个对象被正确地链接,而且没有调用 Foo(..) 时所产生的副作用。一个轻微的缺点是,我们不得不创建新对象,并把旧的扔掉,而不是修改提供给我们的默认既存对象。

    如果有一种标准且可靠地方法来修改既存对象的链接就好了。ES6 之前,有一个非标准的,而且不是完全对所有浏览器通用的方法:通过可以设置的 .__proto__ 属性。ES6中增加了 Object.setPrototypeOf(..) 辅助工具,它提供了标准且可预见的方法。

    1. // ES6 以前
    2. // 扔掉默认既存的 `Bar.prototype`
    3. Bar.prototype = Object.create( Foo.prototype );
    4. // ES6+
    5. // 修改既存的 `Bar.prototype`
    6. Object.setPrototypeOf( Bar.prototype, Foo.prototype );

    如果忽略 Object.create(..) 方式在性能上的轻微劣势(扔掉一个对象,然后被回收),其实它相对短一些而且可能比 ES6+ 的方式更易读。但两种方式可能都只是语法表面现象。

    如果你有一个对象 a 并且希望找到它委托至哪个对象呢(如果有的话)?考察一个实例(一个 JS 对象)的继承血统(在 JS 中是委托链接),在传统的面向类环境中称为 自省(introspection)(或 反射(reflection))。

    考虑下面的代码:

    1. }
    2. Foo.prototype.blah = ...;
    3. var a = new Foo();

    那么我们如何自省 a 来找到它的“祖先”(委托链)呢?一种方式是接受“类”的困惑:

    instanceof 操作符的左侧操作数接收一个普通对象,右侧操作数接收一个 函数instanceof 回答的问题是:a 的整个 [[Prototype]] 链中,有没有出现那个被 Foo.prototype 所随便指向的对象?

    不幸的是,这意味着如果你拥有可以用于测试的 函数Foo,和它带有的 .prototype 引用),你只能查询某些对象(a)的“祖先”。如果你有两个任意的对象,比如 ab,而且你想调查是否 这些对象 通过 [[Prototype]] 链相互关联,单靠 instanceof 帮不上什么忙。

    注意: 如果你使用内建的 .bind(..) 工具来制造一个硬绑定的函数(见第二章),这个被创建的函数将不会拥有 .prototype 属性。将 instanceof 与这样的函数一起使用时,将会透明地替换为创建这个硬绑定函数的 目标函数.prototype

    将硬绑定函数用于“构造器调用”十分罕见,但如果你这么做,它会表现得好像是 目标函数 被调用了,这意味着将 instanceof 与硬绑定函数一起使用也会参照原版函数。

    下面这段代码展示了试图通过“类”的语义和 instanceof 来推导 两个对象 间的关系是多么荒谬:

    1. // 用来检查 `o1` 是否关联到(委托至)`o2` 的帮助函数
    2. function isRelatedTo(o1, o2) {
    3. function F(){}
    4. F.prototype = o2;
    5. return o1 instanceof F;
    6. }
    7. var a = {};
    8. var b = Object.create( a );

    isRelatedTo(..) 内部,我们借用一个一次性的函数 F,重新对它的 .prototype 赋值,使它随意地指向某个对象 o2,之后问 o1 是否是 F 的“一个实例”。很明显,o1 实际上不是继承或遗传自 F,甚至不是由 F 构建的,所以显而易见这种做法是愚蠢且让人困惑的。这个问题归根结底是将类的语义强加于 JavaScript 的尴尬,在这个例子中是由 instanceof 的间接语义揭露的。

    第二种,也是更干净的方式,[[Prototype]] 反射:

    1. Foo.prototype.isPrototypeOf( a ); // true

    注意在这种情况下,我们并不真正关心(甚至 不需要Foo,我们仅需要一个 对象(在我们的例子中被随意标志为 Foo.prototype)来与另一个 对象 测试。isPrototypeOf(..) 回答的问题是:a 的整个 [[Prototype]] 链中, 出现过吗?

    同样的问题,和完全同样的答案。但是在第二种方式中,我们实际上不需要间接地引用一个 .prototype 属性将被自动查询的 函数Foo)。

    1. // 简单地:`b` 在 `c` 的 `[[Prototype]]` 链中出现过吗?
    2. b.isPrototypeOf( c );

    注意,这种方法根本不要求有一个函数(“类”)。它仅仅使用对象的直接引用 bc,来查询他们的关系。换句话说,我们上面的 isRelatedTo(..) 工具是内建在语言中的,它的名字叫 isPrototypeOf(..)

    我们也可以直接取得一个对象的 [[Prototype]]。在 ES5 中,这么做的标准方法是:

    而且你将注意到对象引用是我们期望的:

    1. Object.getPrototypeOf( a ) === Foo.prototype; // true

    大多数浏览器(不是全部!)还一种长期支持的,非标准方法可以访问内部的 [[Prototype]]

    1. a.__proto__ === Foo.prototype; // true

    这个奇怪的 .__proto__(直到 ES6 才被标准化!)属性“魔法般地”取得一个对象内部的 [[Prototype]] 作为引用,如果你想要直接考察(甚至遍历:.__proto__.__proto__...[[Prototype]] 链,这个引用十分有用。

    和我们早先看到的 .constructor 一样,.__proto__ 实际上不存在于你考察的对象上(在我们的例子中是 a)。事实上,它和其他的共通工具在一起(.toString(), .isPrototypeOf(..), 等等),存在于(不可枚举地;见第二章)内建的 Object.prototype 上。

    而且,.__proto__ 虽然看起来像一个属性,但实际上将它看做是一个 getter/setter(见第三章)更合适。

    大致地,我们可以这样描述 .__proto__ 的实现(见第三章,对象属性的定义):

    1. Object.defineProperty( Object.prototype, "__proto__", {
    2. get: function() {
    3. return Object.getPrototypeOf( this );
    4. },
    5. set: function(o) {
    6. // ES6 的 setPrototypeOf(..)
    7. Object.setPrototypeOf( this, o );
    8. return o;
    9. }
    10. } );

    所以,当我们访问 a.__proto__(取得它的值)时,就好像调用 a.__proto__()(调用 getter 函数)一样。虽然 getter 函数存在于 Object.prototype 上(参照第二章,this 绑定规则),但这个函数调用将 a 用作它的 this,所以它相当于在说 Object.getPrototypeOf( a )

    .__proto__ 还是一个可设置的属性,就像早先展示过的 ES6 Object.setPrototypeOf(..)。然而,一般来说你 不应该改变一个既存对象的 [[Prototype]]

    在某些允许对 Array 定义“子类”的框架中,深度地使用了一些非常复杂,高级的技术,但是这在一般的编程实践中经常是让人皱眉头的,因为这通常导致非常难理解/维护的代码。

    注意: 在 ES6 中,关键字 class 将允许某些近似方法,对像 Array 这样的内建类型“定义子类”。参见附录A中关于 ES6 中加入的 class 的讨论。

    仅有一小部分例外(就像前面提到过的)会设置一个默认函数 .prototype 对象的 [[Prototype]],使它引用其他的对象(Object.prototype 之外的对象)。它们会避免将这个默认对象完全替换为一个新的链接对象。否则,为了在以后更容易地阅读你的代码 最好将对象的 [[Prototype]] 链接作为只读性质对待