Uncaught TypeError: Illegal invocation

今天使用JavaScript的setTimeout遇到一个问题。如下是原代码:
 

setTimeout(raiseEle.setCustomValidity, 1000, ""); // raiseEle是一个HTMLInputElement

标题错误是在Chrome中抛出的。

先给出解决方案:

setTimeout(() => raiseEle.setCustomValidity(""), 1000)

看看原来的代码在各浏览器中的报错:

各浏览器报错
SafariTypeError: Con only call HTMLInputElement.setCustomValidity on instances of HTMLInputElement 
ChromeUncaught TypeError: Illegal invocation
EdgeUncaught TypeError: Illegal invocation
FirefoxUncaught TypeError: 'setCustomValidity' called on an object that does not implement interface HTMLInputElement.
OperaUncaught TypeError: Illegal invocation
SeaMonkeyTypeError: 'setCustomValidity' called on an object that does not implement interface HTMLInputElement.

似乎没有在raiseEle上调用该方法?

我尝试过使用以下方案:

setTimeout(raiseEle.setCustomValidity.call, 1000, raiseEle, "")

各浏览器报错
SafariTypeError: Window is not a function
ChromeUncaught TypeError: object is not a function
EdgeUncaught TypeError: object is not a function
FirefoxUncaught TypeError: Function.prototype.call called on incompatible Proxy
OperaUncaught TypeError: object is not a function
SeaMonkeyTypeError: Function.prototype.call called on incompatible Proxy

从Safari的情况来看,难道setTimeOut底层把window当做函数调用了吗?从Chrome、Edge和Opera看,似乎确实将一个对象当做函数调用了。而Firefox的信息似乎更明确,意为在不兼容的代理上调用Function.prototype.call。但没看懂什么意思。

我也尝试过这种方案:

setTimeout(raiseEle.setCustomValidity.apply, 1000, raiseEle, [""])

在各大浏览器中均有报错:

各浏览器报错
SafariTypeError: Window is not a function
ChromeUncaught TypeError: Fonction.prototype.apply was called on #<Window>, which is a object and not a function
EdgeUncaught TypeError: Function.prototype.apply was called on #<Window>, which is a object and not a function
FirefoxUncaught TypeError: Function.prototype.call called on incompatible Proxy
OperaUncaught TypeError: Fonction.prototype.apply was clled on #<window>, which is a object and not a function
SeaMonkeyTypeError: 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中调用函数对象不会理会函数的调用上下文,而直接单独地调用函数体本身。 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值