第五章: 原型 - 对象链接

优质
小牛编辑
138浏览
2023-12-01

对象链接

正如我们看到的,[[Prototype]] 机制是一个内部链接,它存在于一个对象上,这个对象引用一些其他的对象。

这种链接(主要)在对一个对象进行属性/方法引用,但这样的属性/方法不存在时实施。在这种情况下,[[Prototype]] 链接告诉引擎在那个被链接的对象上查找这个属性/方法。接下来,如果这个对象不能满足查询,它的 [[Prototype]] 又会被查找,如此继续。这个在对象间的一系列链接构成了所谓的“原形链”。

创建链接

我们已经彻底揭露了为什么 JavaScript 的 [[Prototype]] 机制和 一样,而且我们也看到了如何在正确的对象间创建 链接

[[Prototype]] 机制的意义是什么?为什么总是见到 JS 开发者们费那么大力气(模拟类)在他们的代码中搞乱这些链接?

记得我们在本章很靠前的地方说过 Object.create(..) 是英雄吗?现在,我们准备好看看为什么了。

  1. var foo = {
  2. something: function() {
  3. console.log( "Tell me something good..." );
  4. }
  5. };
  6. var bar = Object.create( foo );
  7. 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 环境中给我们所需的能力:

  1. if (!Object.create) {
  2. Object.create = function(o) {
  3. function F(){}
  4. F.prototype = o;
  5. return new F();
  6. };
  7. }

这个填补工具通过一个一次性的 F 函数并覆盖它的 .prototype 属性来指向我们想连接到的对象。之后我们用 new F() 构造器调用来制造一个将会链到我们指定对象上的新对象。

Object.create(..) 的这种用法是目前最常见的用法,因为它的这一部分是 可以 填补的。ES5 标准的内建 Object.create(..) 还提供了一个附加的功能,它是 不能 被 ES5 之前的版本填补的。如此,这个功能的使用远没有那么常见。为了完整性,让我们看看这个附加功能:

  1. var anotherObject = {
  2. a: 2
  3. };
  4. var myObject = Object.create( anotherObject, {
  5. b: {
  6. enumerable: false,
  7. writable: true,
  8. configurable: false,
  9. value: 3
  10. },
  11. c: {
  12. enumerable: true,
  13. writable: false,
  14. configurable: false,
  15. value: 4
  16. }
  17. } );
  18. myObject.hasOwnProperty( "a" ); // false
  19. myObject.hasOwnProperty( "b" ); // true
  20. myObject.hasOwnProperty( "c" ); // true
  21. myObject.a; // 2
  22. myObject.b; // 3
  23. myObject.c; // 4

Object.create(..) 的第二个参数通过声明每个新属性的 属性描述符(见第三章)指定了要添加在新对象上的属性。因为在 ES5 之前的环境中填补属性描述符是不可能的,所以 Object.create(..) 的这个附加功能无法填补。

因为 Object.create(..) 的绝大多数用途都是使用填补安全的功能子集,所以大多数开发者在 ES5 之前的环境中使用这种 部分填补 也没有问题。

有些开发者采取严格得多的观点,也就是除非能够被 完全 填补,否则没有函数应该被填补。因为 Object.create(..) 是可以部分填补的工具之一,所以这种较狭窄的观点会说,如果你需要在 ES5 之前的环境中使用 Object.create(..) 的任何功能,你应当使用自定义的工具,而不是填补,而且应当彻底远离使用 Object.create 这个名字。你可以定义自己的工具,比如:

  1. function createAndLinkObject(o) {
  2. function F(){}
  3. F.prototype = o;
  4. return new F();
  5. }
  6. var anotherObject = {
  7. a: 2
  8. };
  9. var myObject = createAndLinkObject( anotherObject );
  10. myObject.a; // 2

我不会分享这种严格的观点。我完全拥护如上面展示的 Object.create(..) 的常见部分填补,甚至在 ES5 之前的环境下在你的代码中使用它。我将选择权留给你。

链接作为候补?

也许这么想很吸引人:这些对象间的链接 主要 是为了给“缺失”的属性和方法提供某种候补。虽然这是一个可观察到的结果,但是我不认为这是考虑 [[Prototype]] 的正确方法。

考虑下面的代码:

  1. var anotherObject = {
  2. cool: function() {
  3. console.log( "cool!" );
  4. }
  5. };
  6. var myObject = Object.create( anotherObject );
  7. myObject.cool(); // "cool!"

得益于 [[Prototype]],这段代码可以工作,但如果你这样写是为了 万一 myObject 不能处理某些开发者可能会调用的属性/方法,而让 anotherObject 作为一个候补,你的软件大概会变得有点儿“魔性”并且更难于理解和维护。

这不是说候补在任何情况下都不是一个合适的设计模式,但它不是一个在 JS 中很常见的用法,所以如果你发现自己在这么做,那么你可能想要退一步并重新考虑它是否真的是合适且合理的设计。

注意: 在 ES6 中,引入了一个称为 Proxy(代理) 的高级功能,它可以提供某种“方法未找到”类型的行为。Proxy 超出了本书的范围,但会在以后的 “你不懂 JS” 系列书目中详细讲解。

这里不要错过一个重要的细节。

例如,你打算为一个开发者设计软件,如果即使在 myObject 上没有 cool() 方法时调用 myObject.cool() 也能工作,会在你的 API 设计上引入一些“魔法”气息,这可能会使未来维护你的软件的开发者很吃惊。

然而你可以在你的 API 设计上少用些“魔法”,而仍然利用 [[Prototype]] 链接的力量。

  1. var anotherObject = {
  2. cool: function() {
  3. console.log( "cool!" );
  4. }
  5. };
  6. var myObject = Object.create( anotherObject );
  7. myObject.doCool = function() {
  8. this.cool(); // internal delegation!
  9. };
  10. myObject.doCool(); // "cool!"

这里,我们调用 myObject.doCool(),它是一个 实际存在于 myObject 上的方法,这使我们的 API 设计更清晰(没那么“魔性”)。在它内部,我们的实现依照 委托设计模式(见第六章),利用 [[Prototype]] 委托到 anotherObject.cool()

换句话说,如果委托是一个内部实现细节,而非在你的 API 结构设计中简单地暴露出来,那么它将倾向于减少意外/困惑。我们会在下一章中详细解释 委托