今天使用JavaScript的setTimeout遇到一个问题。如下是原代码:
setTimeout(raiseEle.setCustomValidity, 1000, ""); // raiseEle是一个HTMLInputElement
标题错误是在Chrome中抛出的。
先给出解决方案:
setTimeout(() => raiseEle.setCustomValidity(""), 1000)
看看原来的代码在各浏览器中的报错:
Safari | TypeError: Con only call HTMLInputElement.setCustomValidity on instances of HTMLInputElement |
Chrome | Uncaught TypeError: Illegal invocation |
Edge | Uncaught TypeError: Illegal invocation |
Firefox | Uncaught TypeError: 'setCustomValidity' called on an object that does not implement interface HTMLInputElement. |
Opera | Uncaught TypeError: Illegal invocation |
SeaMonkey | TypeError: 'setCustomValidity' called on an object that does not implement interface HTMLInputElement. |
似乎没有在raiseEle上调用该方法?
我尝试过使用以下方案:
setTimeout(raiseEle.setCustomValidity.call, 1000, raiseEle, "")
Safari | TypeError: Window is not a function |
Chrome | Uncaught TypeError: object is not a function |
Edge | Uncaught TypeError: object is not a function |
Firefox | Uncaught TypeError: Function.prototype.call called on incompatible Proxy |
Opera | Uncaught TypeError: object is not a function |
SeaMonkey | TypeError: Function.prototype.call called on incompatible Proxy |
从Safari的情况来看,难道setTimeOut底层把window当做函数调用了吗?从Chrome、Edge和Opera看,似乎确实将一个对象当做函数调用了。而Firefox的信息似乎更明确,意为在不兼容的代理上调用Function.prototype.call。但没看懂什么意思。
我也尝试过这种方案:
setTimeout(raiseEle.setCustomValidity.apply, 1000, raiseEle, [""])
在各大浏览器中均有报错:
Safari | TypeError: Window is not a function |
Chrome | Uncaught TypeError: Fonction.prototype.apply was called on #<Window>, which is a object and not a function |
Edge | Uncaught TypeError: Function.prototype.apply was called on #<Window>, which is a object and not a function |
Firefox | Uncaught TypeError: Function.prototype.call called on incompatible Proxy |
Opera | Uncaught TypeError: Fonction.prototype.apply was clled on #<window>, which is a object and not a function |
SeaMonkey | TypeError: Function.prototype.call called on incompatible Proxy |
从这次结果来看,问题似乎很明显了。在确认答案之前我们不妨给出自己的见解。我们这两次传参都是将函数对象直接传给setTimeOut,并希望在回调时像我们传入那样调用:
// 尝试1
raiseEle.setCustomValidity("");
// 尝试2
raiseEle.setCustomValidity.call(raiseEle, "");
// 尝试3
raiseEle.setCustomValidity.apply(raiseEle, [""]);
但实际上传入的是函数本身,并没有绑定(bound)调用函数的对象,此函数在setTimeout中的调用也许更像这样:
// 尝试1
Global.setCustomValidity("");
// 尝试2
Global.call(raiseEle, "");
// 尝试3
Global.apply(raiseEle, [""]);
这里的Global是运行时从最开始就创建的对象,在浏览器中通常为window。我们在node.js中测试一下:
let test = {
callback(){
console.log("callback")
}
}
setTimeout(test.callback, 100)
但结果和我预想的不同,各浏览器包括node.js都能顺利找到test.callback的调用者。
难道是因为函数在prototype上的缘故吗?
function Test(){
this.id = Math.random()
}
Test.prototype.callback = function(){
console.log("callback", this.id);
}
setTimeout((new Test()).callback, 1000)
setTimeout(() => (new Test()).callback(), 2000)
这次不仅将函数定义在了对象的prototype上,还在构造函数中为实例添加了id属性。结果1秒后调用log出的id属性为undefined,而2秒后log出的id属性没问题。
以上两个示例说明定义在对象中的函数,其this已经死死绑定到改对象,而在prototype中定义的函数没有绑定this到实例,在setTimeout调用时也不会将调用者当做this传入,而是仅仅将函数体本身调用一次。
为了验证结论,我翻开了红宝书:
对象中方法的this会被解析为该对象。
这个this和调用上下文无关,无论该函数如何被调用,或者用一个指针指向该函数再调用指针,该方法也会使用正确的this。
以下代码证实结论:
let mmp = (new Test()).callback;
mmp();
let nnp = () => (new Test()).callback()
nnp();
mmp函数输出this.id值为undefined,而nnp函数输出this.id值为一个浮点数。
这样很容易解释为什么第二种和第三种方案也会报错了。因为window对象中没有call和apply函数,或者window对象不是一个Function,不能调用call和apply来调用,因此才会出现诸如window对象不是函数以及Function.prototype.call和Function.prototype.apply调用使用不正确的代理这些错误。
结论:
在setTimeout中调用函数对象不会理会函数的调用上下文,而直接单独地调用函数体本身。