我们都知道 call()
和 apply()
是用来改变函数中 this
指向的,它们的共同点是都会立即执行,而如果问到它们之间有什么区别时,我们都会想到一个「传参不同」
call
参数要分开传,比如call(this, 1, 2, 3, ...)apply
传递参数是数组形式,比如apply(this, [1,2,3,...])
以上就是一直以来我对两者的全部认知了,直到有天我在 Vue 的源码中看到了这么一段"无意义"的代码:
于是我开始猜想,应该是两者之间还存在着不为人知的性能差异?为了佐证这点,赶紧写了个循环试一下:
代码语言:javascript复制let arr = [10,12,123,432,54,67,678,98,342]; // 随便定义一些参数
function fn () {}
const name = 'call'
// const name = 'apply'
console.time(name);
for (let i = 0; i < 99999999; i ) {
fn[name](this, ...arr) // call
// fn[name](this, arr) // apply
}
console.timeEnd(name)
console.time
和console.timeEnd
是很方便的调试技巧。
测试结果如下:
以上测试是带了参数的,下面不传参数,只绑定this测试下:
从结果看好像差异也不是很明显,当时觉得可能是测试数据比较简单吧(其实并不是,后面会说到),不过从平均值来看,还是感觉 call
稍微比 apply
更稳定一些。
以上两组对照都是在谷歌浏览器下进行的,于是我就想在苹果 Safari 浏览器下会是什么结果呢?
结果是非常的Amazing啊,首先一模一样的数据规格,Safari的表现比谷歌差了好多,但是想到我的Safari版本可能比较低(MacOS版本10.15.7),所以执行效率差异这个先按下不表,最主要是这个结果怎么跟谷歌是反过来的,反而 apply
要快很多啊?
一定是我的问题!人一旦清楚认知自己是菜鸟的事实,往往就能很快作出准确的判断。于是我仔细查看刚刚的代码,突然意识到,我在往 call
传参的时候习惯性地使用了 es6
的展开运算符,在 babeljs 这个网站上看看 babel 会如何处理上面的代码:
可以看到使用了解构传参的 call
方法经过了 babel 的转译,甚至还多调用了一次 apply
,反而变得复杂了,虽然浏览器具体怎么处理我们不得而知,但还是可以看出来解构参数这一步操作的消耗可能蛮大的,于是我改成了正常的传参,像这样:
let arr = [10,12,123,432,54,67,678,98,342];
function fn () {}
const name = 'call'
// const name = 'apply'
console.time(name);
for (let i = 0; i < 99999999; i ) {
fn[name](this, 10,12,123,432,54,67,678,98,342) // call 这里把参数复制下来传参了
// fn[name](this, arr) // apply
}
console.timeEnd(name)
再重新跑一遍对照,果然Safari的表现就正常了:
再重新看下谷歌浏览器的对照结果,这下就非常明显了,差距一下拉开了几条街:
到这里我们总算是可以得出结论,call
的性能比 apply
要好。如果上面属于实践出真知,那么下面就该说说原理。在探索过程中我查阅了许多资料,最终还得是 ECMA 上对于两个方法的规范提案解答了我的疑惑,虽然不同的浏览器对于JS规范做出的具体实现是不一样的,但毕竟都遵循着同样的规范,通过它我们就能看清楚本质。
从规范中我们不难看出,apply
在处理参数上很明显比 call
多了两个步骤,但它们却都调用了同一个方法 PrepareForTailCall
,而且返回的结果也是同个方法只不过传的第三个参数略有不同而已,所以具体的实现上肯定也是 call
比较纯粹,而 apply
则只是为了方便传递参数而创造的方法,这足以证明 call
性能要优于 apply
。
以上就是文章的全部内容,希望对你有所帮助!如果觉得文章写的不错,可以点赞收藏,也欢迎关注,我会持续更新更多前端有用的知识与实用技巧,我是茶无味de一天,希望与你共同成长~