内省
如果你写过许多面向类的程序(无论是使用JavaScript还是其他语言),那你可能很熟悉自省。自省就是检查实例的类型。类实例的自省主要目的是通过创建方式来判断对象的结构和功能。
下面的代码使用instanceof来推测对象a1的功能:
因为Foo.prototype(不是Foo!)在a1的[[Prototype]]链上,所以instanceof操作(会令人困惑地)告诉我们a1是Foo“类”的一个实例。知道了这点后,我们就可以认为a1有Foo“类”描述的功能。
当然,Foo类并不存在,只有一个普通的函数Foo,它引用了a1委托的对象(Foo.prototype)。从语法角度来说,instanceof似乎是检查a1和Foo的关系,但是实际上它想说的是a1和Foo.prototype(引用的对象)是互相关联的。
instanceof语法会产生语义困惑而且非常不直观。如果你想检查对象a1和某个对象的关系,那必须使用另一个引用该对象的函数才行——你不能直接判断两个对象是否关联。
简单来说是这样的:
如果要使用instanceof和.prototype语义来检查本例中实体的关系,那必须这样做:
显然这是一种非常糟糕的方法。举例来说,(使用类时)你最直观的想法可能是使用BarinstanceofFoo(因为很容易把“实例”理解成“继承”),但是在JavaScript中这是行不通的,你必须使用Bar.prototypeinstanceofFoo。
还有一种常见但是可能更加脆弱的内省模式,许多开发者认为它比instanceof更好。这种模式被称为“鸭子类型”。这个术语源自这句格言“如果看起来像鸭子,叫起来像鸭子,那就一定是鸭子。”
举例来说:
if(a1.something){
a1.something();
}
我们并没有检查a1和委托something()函数的对象之间的关系,而是假设如果a1通过了测试a1.something的话,那a1就一定能调用.something()(无论这个方法存在于a1自身还是委托到其他对象)。这个假设的风险其实并不算很高。
但是“鸭子类型”通常会在测试之外做出许多关于对象功能的假设,这当然会带来许多风险(或者说脆弱的设计)。
ES6的Promise就是典型的“鸭子类型”
出于各种各样的原因,我们需要判断一个对象引用是否是Promise,但是判断的方法是检查对象是否有then()方法。换句话说,如果对象有then()方法,ES6的Promise就会认为这个对象是“可持续”(thenable)的,因此会期望它具有Promise的所有标准行为。
如果有一个不是Promise但是具有then()方法的对象,那你千万不要把它用在ES6的Promise机制中,否则会出错。
这个例子清楚地解释了“鸭子类型”的危害。你应该尽量避免使用这个方法,即使使用也要保证条件是可控的。
对象关联风格代码,其内省更加简洁。我们先来回顾一下之前的Foo/Bar/b1对象关联例子(只包含关键代码):
使用对象关联时,所有的对象都是通过[[Prototype]]委托互相关联,下面是内省的方法,非常简单:
我们没有使用instanceof,因为它会产生一些和类有关的误解。现在我们想问的问题是“你是我的原型吗?”我们并不需要使用间接的形式,比如Foo.prototype或者繁琐的Foo.prototype.isPrototypeOf(..)。
我觉得和之前的方法比起来,这种方法显然更加简洁并且清晰。再说一次,我们认为JavaScript中对象关联比类风格的代码更加简洁(而且功能相同)。