第六章: 行为委托 - 自省
自省
如果你花了很长时间在面向类的编程方式(不管是 JS 还是其他的语言)上,你可能会对 类型自省 很熟悉:自省一个实例来找出它是什么 种类 的对象。在类的实例上进行 类型自省 的主要目的是根据 对象是如何创建的 来推断它的结构/能力。
考虑这段代码,它使用 instanceof
(见第五章)来自省一个对象 a1
来推断它的能力:
function Foo() {
// ...
}
Foo.prototype.something = function(){
// ...
}
var a1 = new Foo();
// 稍后
if (a1 instanceof Foo) {
a1.something();
}
因为 Foo.prototype
(不是 Foo
!)在 a1
的 [[Prototype]]
链上(见第五章),instanceof
操作符(使人困惑地)假装告诉我们 a1
是一个 Foo
“类”的实例。有了这个知识,我们假定 a1
有 Foo
“类”中描述的能力。
当然,这里没有 Foo
类,只有一个普通的函数 Foo
,它恰好拥有一个引用指向一个随意的对象(Foo.prototype
),而 a1
恰好委托链接至这个对象。通过它的语法,instanceof
假装检查了 a1
和 Foo
之间的关系,但它实际上告诉我们的是 a1
和 Foo.prototype
(这个随意被引用的对象)是否有关联。
instanceof
在语义上的混乱(和间接)意味着,要使用以 instanceof
为基础的自省来查询对象 a1
是否与讨论中的对象有关联,你 不得不 拥有一个持有对这个对象引用的函数 —— 你不能直接查询这两个对象是否有关联。
回想本章前面的抽象 Foo
/ Bar
/ b1
例子,我们在这里缩写一下:
function Foo() { /* .. */ }
Foo.prototype...
function Bar() { /* .. */ }
Bar.prototype = Object.create( Foo.prototype );
var b1 = new Bar( "b1" );
为了在这个例子中的实体上进行 类型自省, 使用 instanceof
和 .prototype
语义,这里有各种你可能需要实施的检查:
// `Foo` 和 `Bar` 互相的联系
Bar.prototype instanceof Foo; // true
Object.getPrototypeOf( Bar.prototype ) === Foo.prototype; // true
Foo.prototype.isPrototypeOf( Bar.prototype ); // true
// `b1` 与 `Foo` 和 `Bar` 的联系
b1 instanceof Foo; // true
b1 instanceof Bar; // true
Object.getPrototypeOf( b1 ) === Bar.prototype; // true
Foo.prototype.isPrototypeOf( b1 ); // true
Bar.prototype.isPrototypeOf( b1 ); // true
可以说,其中有些烂透了。举个例子,直觉上(用类)你可能想说这样的东西 Bar instanceof Foo
(因为很容易混淆“实例”的意义认为它包含“继承”),但在 JS 中这不是一个合理的比较。你不得不说 Bar.prototype instanceof Foo
。
另一个常见,但也许健壮性更差的 类型自省 模式叫“duck typing(鸭子类型)”,比起 instanceof
来许多开发者都倾向于它。这个术语源自一则谚语,“如果它看起来像鸭子,叫起来像鸭子,那么它一定是一只鸭子”。
例如:
if (a1.something) {
a1.something();
}
与其检查 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 示例:
var Foo = { /* .. */ };
var Bar = Object.create( Foo );
Bar...
var b1 = Object.create( Bar );
使用这种 OLOO 方式,我们所拥有的一切都是通过 [[Prototype]]
委托关联起来的普通对象,这是我们可能会用到的大幅简化后的 类型自省:
// `Foo` 和 `Bar` 互相的联系
Foo.isPrototypeOf( Bar ); // true
Object.getPrototypeOf( Bar ) === Foo; // true
// `b1` 与 `Foo` 和 `Bar` 的联系
Foo.isPrototypeOf( b1 ); // true
Bar.isPrototypeOf( b1 ); // true
Object.getPrototypeOf( b1 ) === Bar; // true
我们不再使用 instanceof
,因为它令人迷惑地假装与类有关系。现在,我们只需要(非正式地)问这个问题,“你是我的 一个 原型吗?”。不再需要用 Foo.prototype
或者痛苦冗长的 Foo.prototype.isPrototypeOf(..)
来间接地查询了。
我想可以说这些检查比起前面一组自省检查,极大地减少了复杂性/混乱。又一次,我们看到了在 JavaScript 中 OLOO 要比类风格的编码简单(但有着相同的力量)。