考虑这段代码,它使用 (见第五章)来自省一个对象 a1
来推断它的能力:
因为 Foo.prototype
(不是 Foo
!)在 a1
的 [[Prototype]]
链上(见第五章),instanceof
操作符(使人困惑地)假装告诉我们 a1
是一个 Foo
“类”的实例。有了这个知识,我们假定 a1
有 Foo
“类”中描述的能力。
当然,这里没有 Foo
类,只有一个普通的函数 Foo
,它恰好拥有一个引用指向一个随意的对象(Foo.prototype
),而 a1
恰好委托链接至这个对象。通过它的语法,instanceof
假装检查了 a1
和 Foo
之间的关系,但它实际上告诉我们的是 a1
和 Foo.prototype
(这个随意被引用的对象)是否有关联。
instanceof
在语义上的混乱(和间接)意味着,要使用以 为基础的自省来查询对象 a1
是否与讨论中的对象有关联,你 不得不 拥有一个持有对这个对象引用的函数 —— 你不能直接查询这两个对象是否有关联。
回想本章前面的抽象 Foo
/ Bar
/ b1
例子,我们在这里缩写一下:
Foo.prototype...
function Bar() { /* .. */ }
Bar.prototype = Object.create( Foo.prototype );
可以说,其中有些烂透了。举个例子,直觉上(用类)你可能想说这样的东西 Bar instanceof Foo
(因为很容易混淆“实例”的意义认为它包含“继承”),但在 JS 中这不是一个合理的比较。你不得不说 Bar.prototype instanceof Foo
。
另一个常见,但也许健壮性更差的 类型自省 模式叫“duck typing(鸭子类型)”,比起 instanceof
来许多开发者都倾向于它。这个术语源自一则谚语,“如果它看起来像鸭子,叫起来像鸭子,那么它一定是一只鸭子”。
例如:
if (a1.something) {
a1.something();
}
与其检查 a1
和一个持有可委托的 函数的对象的关系,我们假设 a1.something
测试通过意味着 a1
有能力调用 .something()
(不管是直接在 a1
上直接找到方法,还是委托至其他对象)。就其本身而言,这种假设没什么风险。
但是“鸭子类型”常常被扩展用于 除了被测试关于对象能力以外的其他假设,这当然会在测试中引入更多风险(比如脆弱的设计)。
由于种种原因,需要判定任意一个对象引用是否 是一个 Promise,但测试是通过检查对象是否恰好有 then()
函数出现在它上面来完成的。换句话说,如果任何对象 恰好有一个 then()
方法,ES6 的 Promises 将会无条件地假设这个对象 是“thenable” 的,而且因此会期望它按照所有的 Promises 标准行为那样一致地动作。
如果你有任何非 Promise 对象,而却不管因为什么它恰好拥有 then()
方法,你会被强烈建议使它远离 ES6 的 Promise 机制,来避免破坏这种假设。
这个例子清楚地展现了“鸭子类型”的风险。你应当仅在可控的条件下,保守地使用这种方式。
再次将我们的注意力转向本章中出现的 OLOO 风格的代码,类型自省 变得清晰多了。让我们回想(并缩写)本章的 Foo
/ Bar
/ b1
的 OLOO 示例:
使用这种 OLOO 方式,我们所拥有的一切都是通过 [[Prototype]]
委托关联起来的普通对象,这是我们可能会用到的大幅简化后的 类型自省:
// `Foo` 和 `Bar` 互相的联系
Foo.isPrototypeOf( Bar ); // true
// `b1` 与 `Foo` 和 `Bar` 的联系
Foo.isPrototypeOf( b1 ); // true
Bar.isPrototypeOf( b1 ); // true
Object.getPrototypeOf( b1 ) === Bar; // true
我想可以说这些检查比起前面一组自省检查,极大地减少了复杂性/混乱。又一次,我们看到了在 JavaScript 中 OLOO 要比类风格的编码简单(但有着相同的力量)。