Thenable鸭子类型(Duck Typing)
在Promise的世界中,一个重要的细节是如何确定一个值是否是纯粹的Promise。或者更直接地说,一个值会不会像Promise那样动作?
我们知道Promise是由new Promise(..)
语法构建的,你可能会想p instanceof Promise
将是一个可以接受的检查。但不幸的是,有几个理由表明它不是完全够用。
主要原因是,你可以从其他浏览器窗口中收到Promise值(iframe等),其他的浏览器窗口会拥有自己的不同于当前窗口/frame的Promise,这种检查将会在定位Promise实例时失效。
另外,一个库或框架可能会选择实现自己的Promise而不是用ES6原生的Promise
实现。事实上,你很可能在根本没有Promise的老版本浏览器中通过一个库来使用Promise。
当我们在本章稍后讨论Promise的解析过程时,为什么识别并同化一个非纯种但相似Promise的值仍然很重要会愈发明显。但目前只需要相信我,它是拼图中很重要的一块。
如此,人们决定识别一个Promise(或像Promise一样动作的某些东西)的方法是定义一种称为“thenable”的东西,也就是任何拥有then(..)
方法的对象或函数。这种方法假定任何这样的值都是一个符合Promise的thenable。
根据值的形状(存在什么属性)来推测它的“类型”的“类型检查”有一个一般的名称,称为“鸭子类型检查”——“如果它看起来像一只鸭子,并且叫起来像一只鸭子,那么它一定是一只鸭子”(参见本丛书的 类型与文法)。所以对thenable的鸭子类型检查可能大致是这样:
if (
p !== null &&
(
typeof p === "object" ||
typeof p === "function"
) &&
typeof p.then === "function"
) {
// 认为它是一个thenable!
}
else {
// 不是一个thenable
}
晕!先把将这种逻辑在各种地方实现有点丑陋的事实放在一边不谈,这里还有更多更深层的麻烦。
如果你试着用一个偶然拥有then(..)
函数的任意对象/函数来完成一个Promise,但你又没想把它当做一个Promise/thenable来对待,你的运气就用光了,因为它会被自动地识别为一个thenable并以特殊的规则来对待(见本章后面的部分)。
如果你不知道一个值上面拥有then(..)
就更是这样。比如:
var o = { then: function(){} };
// 使`v`用`[[Prototype]]`链接到`o`
var v = Object.create( o );
v.someStuff = "cool";
v.otherStuff = "not so cool";
v.hasOwnProperty( "then" ); // false
v
看起来根本不像是一个Promise或thenable。它只是一个拥有一些属性的直白的对象。你可能只是想要把这个值像其他对象那样传递而已。
但你不知道的是,v
还[[Prototype]]
连接着(见本丛书的 this与对象原型)另一个对象o
,在它上面偶然拥有一个then(..)
。所以thenable鸭子类型检查将会认为并假定v
是一个thenable。噢。
它甚至不需要直接故意那么做:
Object.prototype.then = function(){};
Array.prototype.then = function(){};
var v1 = { hello: "world" };
var v2 = [ "Hello", "World" ];
v1
和v2
都将被假定为是thenalbe的。你不能控制或预测是否有其他代码偶然或恶意地将then(..)
加到Object.prototype
,Array.prototype
,或其他任何原生原型上。而且如果这个指定的函数并不将它的任何参数作为回调调用,那么任何用这样的值被解析的Promise都将无声地永远挂起!疯狂。
听起来难以置信或不太可能?也许。
要知道,在ES6之前就有几种广为人知的非Promise库在社区中存在了,而且它们已经偶然拥有了称为then(..)
的方法。这些库中的一些选择了重命名它们自己的方法来回避冲突(这很烂!)。另一些则因为它们无法改变来回避冲突,简单地降级为“不兼容基于Promise的代码”的不幸状态。
用来劫持原先非保留的——而且听起来完全是通用的——then
属性名称的标准决议是,没有值(或它的任何委托),无论是过去,现在,还是将来,可以拥有then(..)
函数,不管是有意的还是偶然的,否则这个值将在Promise系统中被混淆为一个thenable,从而可能产生非常难以追踪的Bug。
警告: 我不喜欢我们用thenable的鸭子类型来结束对Promise认知的方式。还有其他的选项,比如“branding”或者甚至是“anti-branding”;我们得到的似乎是一个最差劲儿的妥协。但它并不全是悲观与失望。thenable鸭子类型可以很有用,就像我们马上要看到的。只是要小心,如果thenable鸭子类型将不是Promise的东西误认为是Promise,它就可能成为灾难。