第五章: 原型 - 对象链接
对象链接
正如我们看到的,[[Prototype]]
机制是一个内部链接,它存在于一个对象上,这个对象引用一些其他的对象。
这种链接(主要)在对一个对象进行属性/方法引用,但这样的属性/方法不存在时实施。在这种情况下,[[Prototype]]
链接告诉引擎在那个被链接的对象上查找这个属性/方法。接下来,如果这个对象不能满足查询,它的 [[Prototype]]
又会被查找,如此继续。这个在对象间的一系列链接构成了所谓的“原形链”。
创建链接
我们已经彻底揭露了为什么 JavaScript 的 [[Prototype]]
机制和 类 不 一样,而且我们也看到了如何在正确的对象间创建 链接。
[[Prototype]]
机制的意义是什么?为什么总是见到 JS 开发者们费那么大力气(模拟类)在他们的代码中搞乱这些链接?
记得我们在本章很靠前的地方说过 Object.create(..)
是英雄吗?现在,我们准备好看看为什么了。
var foo = {
something: function() {
console.log( "Tell me something good..." );
}
};
var bar = Object.create( foo );
bar.something(); // Tell me something good...
Object.create(..)
创建了一个链接到我们指定的对象(foo
)上的新对象(bar
),这给了我们 [[Prototype]]
机制的所有力量(委托),而且没有 new
函数作为类和构造器调用产生的所有没必要的复杂性,搞乱 .prototype
和 .constructor
引用,或任何其他的多余的东西。
注意: Object.create(null)
创建一个拥有空(也就是 null
)[[Prototype]]
链接的对象,如此这个对象不能委托到任何地方。因为这样的对象没有原形链,instancof
操作符(前 面解释过)没有东西可检查,所以它总返回 false
。由于他们典型的用途是在属性中存储数据,这种特殊的空 [[Prototype]]
对象经常被称为“字典(dictionaries)”,这主要是因为它们不可能受到在 [[Prototype]]
链上任何委托属性/函数的影响,所以它们是纯粹的扁平数据存储。
我们不 需要 类来在两个对象间创建有意义的关系。我们需要 真正关心 的唯一问题是对象为了委托而链接在一起,而 Object.create(..)
给我们这种链接并且没有一切关于类的烂设计。
填补 Object.create()
Object.create(..)
在 ES5 中被加入。你可能需要支持 ES5 之前的环境(比如老版本的 IE),所以让我们来看一个 Object.create(..)
的简单 部分 填补工具,它甚至能在更老的 JS 环境中给我们所需的能力:
if (!Object.create) {
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
};
}
这个填补工具通过一个一次性的 F
函数并覆盖它的 .prototype
属性来指向我们想连接到的对象。之后我们用 new F()
构造器调用来制造一个将会链到我们指定对象上的新对象。
Object.create(..)
的这种用法是目前最常见的用法,因为它的这一部分是 可以 填补的。ES5 标准的内建 Object.create(..)
还提供了一个附加的功能,它是 不能 被 ES5 之前的版本填补的。如此,这个功能的使用远没有那么常见。为了完整性,让我们看看这个附加功能:
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject, {
b: {
enumerable: false,
writable: true,
configurable: false,
value: 3
},
c: {
enumerable: true,
writable: false,
configurable: false,
value: 4
}
} );
myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true
myObject.hasOwnProperty( "c" ); // true
myObject.a; // 2
myObject.b; // 3
myObject.c; // 4
Object.create(..)
的第二个参数通过声明每个新属性的 属性描述符(见第三章)指定了要添加在新对象上的属性。因为在 ES5 之前的环境中填补属性描述符是不可能的,所以 Object.create(..)
的这个附加功能无法填补。
因为 Object.create(..)
的绝大多数用途都是使用填补安全的功能子集,所以大多数开发者在 ES5 之前的环境中使用这种 部分填补 也没有问题。
有些开发者采取严格得多的观点,也就是除非能够被 完全 填补,否则没有函数应该被填补。因为 Object.create(..)
是可以部分填补的工具之一,所以这种较狭窄的观点会说,如果你需要在 ES5 之前的环境中使用 Object.create(..)
的任何功能,你应当使用自定义的工具,而不是填补,而且应当彻底远离使用 Object.create
这个名字。你可以定义自己的工具,比如:
function createAndLinkObject(o) {
function F(){}
F.prototype = o;
return new F();
}
var anotherObject = {
a: 2
};
var myObject = createAndLinkObject( anotherObject );
myObject.a; // 2
我不会分享这种严格的观点。我完全拥护如上面展示的 Object.create(..)
的常见部分填补,甚至在 ES5 之前的环境下在你的代码中使用它。我将选择权留给你。
链接作为候补?
也许这么想很吸引人:这些对象间的链接 主要 是为了给“缺失”的属性和方法提供某种候补。虽然这是一个可观察到的结果,但是我不认为这是考虑 [[Prototype]]
的正确方法。
考虑下面的代码:
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.cool(); // "cool!"
得益于 [[Prototype]]
,这段代码可以工作,但如果你这样写是为了 万一 myObject
不能处理某些开发者可能会调用的属性/方法,而让 anotherObject
作为一个候补,你的软件大概会变得有点儿“魔性”并且更难于理解和维护。
这不是说候补在任何情况下都不是一个合适的设计模式,但它不是一个在 JS 中很常见的用法,所以如果你发现自己在这么做,那么你可能想要退一步并重新考虑它是否真的是合适且合理的设计。
注意: 在 ES6 中,引入了一个称为 Proxy(代理)
的高级功能,它可以提供某种“方法未找到”类型的行为。Proxy
超出了本书的范围,但会在以后的 “你不懂 JS” 系列书目中详细讲解。
这里不要错过一个重要的细节。
例如,你打算为一个开发者设计软件,如果即使在 myObject
上没有 cool()
方法时调用 myObject.cool()
也能工作,会在你的 API 设计上引入一些“魔法”气息,这可能会使未来维护你的软件的开发者很吃惊。
然而你可以在你的 API 设计上少用些“魔法”,而仍然利用 [[Prototype]]
链接的力量。
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.doCool = function() {
this.cool(); // internal delegation!
};
myObject.doCool(); // "cool!"
这里,我们调用 myObject.doCool()
,它是一个 实际存在于 myObject
上的方法,这使我们的 API 设计更清晰(没那么“魔性”)。在它内部,我们的实现依照 委托设计模式(见第六章),利用 [[Prototype]]
委托到 anotherObject.cool()
。
换句话说,如果委托是一个内部实现细节,而非在你的 API 结构设计中简单地暴露出来,那么它将倾向于减少意外/困惑。我们会在下一章中详细解释 委托。