自省

如果你花了很长时间在面向类的编程方式(不管是 JS 还是其他的语言)上,你可能会对 类型自省 很熟悉:自省一个实例来找出它是什么 种类 的对象。在类的实例上进行 类型自省 的主要目的是根据 对象是如何创建的 来推断它的结构/能力。

考虑这段代码,它使用 instanceof(见第五章)来自省一个对象 a1 来推断它的能力:

  1. function Foo() {
  2. // ...
  3. }
  4. Foo.prototype.something = function(){
  5. // ...
  6. }
  7. var a1 = new Foo();
  8. // 稍后
  9. if (a1 instanceof Foo) {
  10. a1.something();
  11. }

因为 Foo.prototype(不是 Foo!)在 a1[[Prototype]] 链上(见第五章),instanceof 操作符(使人困惑地)假装告诉我们 a1 是一个 Foo “类”的实例。有了这个知识,我们假定 a1Foo “类”中描述的能力。

当然,这里没有 Foo 类,只有一个普通的函数 Foo,它恰好拥有一个引用指向一个随意的对象(Foo.prototype),而 a1 恰好委托链接至这个对象。通过它的语法,instanceof 假装检查了 a1Foo 之间的关系,但它实际上告诉我们的是 a1Foo.prototype(这个随意被引用的对象)是否有关联。

instanceof 在语义上的混乱(和间接)意味着,要使用以 instanceof 为基础的自省来查询对象 a1 是否与讨论中的对象有关联,你 不得不 拥有一个持有对这个对象引用的函数 —— 你不能直接查询这两个对象是否有关联。

回想本章前面的抽象 Foo / Bar / b1 例子,我们在这里缩写一下:

  1. function Foo() { /* .. */ }
  2. Foo.prototype...
  3. function Bar() { /* .. */ }
  4. Bar.prototype = Object.create( Foo.prototype );
  5. var b1 = new Bar( "b1" );

为了在这个例子中的实体上进行 类型自省, 使用 instanceof.prototype 语义,这里有各种你可能需要实施的检查:

  1. // `Foo` 和 `Bar` 互相的联系
  2. Bar.prototype instanceof Foo; // true
  3. Object.getPrototypeOf( Bar.prototype ) === Foo.prototype; // true
  4. Foo.prototype.isPrototypeOf( Bar.prototype ); // true
  5. // `b1` 与 `Foo` 和 `Bar` 的联系
  6. b1 instanceof Foo; // true
  7. b1 instanceof Bar; // true
  8. Object.getPrototypeOf( b1 ) === Bar.prototype; // true
  9. Foo.prototype.isPrototypeOf( b1 ); // true
  10. Bar.prototype.isPrototypeOf( b1 ); // true

可以说,其中有些烂透了。举个例子,直觉上(用类)你可能想说这样的东西 Bar instanceof Foo(因为很容易混淆“实例”的意义认为它包含“继承”),但在 JS 中这不是一个合理的比较。你不得不说 Bar.prototype instanceof Foo

另一个常见,但也许健壮性更差的 类型自省 模式叫“duck typing(鸭子类型)”,比起 instanceof 来许多开发者都倾向于它。这个术语源自一则谚语,“如果它看起来像鸭子,叫起来像鸭子,那么它一定是一只鸭子”。

例如:

  1. if (a1.something) {
  2. a1.something();
  3. }

与其检查 a1 和一个持有可委托的 something() 函数的对象的关系,我们假设 a1.something 测试通过意味着 a1 有能力调用 .something()(不管是直接在 a1 上直接找到方法,还是委托至其他对象)。就其本身而言,这种假设没什么风险。

但是“鸭子类型”常常被扩展用于 除了被测试关于对象能力以外的其他假设,这当然会在测试中引入更多风险(比如脆弱的设计)。

“鸭子类型”的一个值得注意的例子来自于 ES6 的 Promises(就是我们前面解释过,将不再本书内涵盖的内容)。

由于种种原因,需要判定任意一个对象引用是否 是一个 Promise,但测试是通过检查对象是否恰好有 then() 函数出现在它上面来完成的。换句话说,如果任何对象 恰好有一个 then() 方法,ES6 的 Promises 将会无条件地假设这个对象 是“thenable” 的,而且因此会期望它按照所有的 Promises 标准行为那样一致地动作。

如果你有任何非 Promise 对象,而却不管因为什么它恰好拥有 then() 方法,你会被强烈建议使它远离 ES6 的 Promise 机制,来避免破坏这种假设。

这个例子清楚地展现了“鸭子类型”的风险。你应当仅在可控的条件下,保守地使用这种方式。

再次将我们的注意力转向本章中出现的 OLOO 风格的代码,类型自省 变得清晰多了。让我们回想(并缩写)本章的 Foo / Bar / b1 的 OLOO 示例:

  1. var Foo = { /* .. */ };
  2. var Bar = Object.create( Foo );
  3. Bar...
  4. var b1 = Object.create( Bar );

使用这种 OLOO 方式,我们所拥有的一切都是通过 [[Prototype]] 委托关联起来的普通对象,这是我们可能会用到的大幅简化后的 类型自省

  1. // `Foo` 和 `Bar` 互相的联系
  2. Foo.isPrototypeOf( Bar ); // true
  3. Object.getPrototypeOf( Bar ) === Foo; // true
  4. // `b1` 与 `Foo` 和 `Bar` 的联系
  5. Foo.isPrototypeOf( b1 ); // true
  6. Bar.isPrototypeOf( b1 ); // true
  7. Object.getPrototypeOf( b1 ) === Bar; // true

我们不再使用 instanceof,因为它令人迷惑地假装与类有关系。现在,我们只需要(非正式地)问这个问题,“你是我的 一个 原型吗?”。不再需要用 Foo.prototype 或者痛苦冗长的 Foo.prototype.isPrototypeOf(..) 来间接地查询了。

我想可以说这些检查比起前面一组自省检查,极大地减少了复杂性/混乱。又一次,我们看到了在 JavaScript 中 OLOO 要比类风格的编码简单(但有着相同的力量)。