用法
在JavaScript中,我们经常使用apply,call,bind来绑定this的指向,这是this绑定中的硬绑定,即绑定后无法修改。(下面有些代码会涉及this绑定的内容,有关this绑定的内容可以看看我的另一篇博客-->理解掌握JavaScript的this的指向)
首先我们看看下面的代码
function fn(num1, num2, num3) {
console.log(`a: ${this.a} sum: ${num1+num2+num3}`);
}
var a = 1;
var obj = {
a: 2
}
fn(1, 2, 3);//a: 1 sum: 6
fn.call(obj, 1, 2, 3);//a: 2 sum: 6
fn.apply(obj, [1, 2, 3]);//a: 2 sum: 6
fn.bind(obj, 1, 2, 3)();//a: 2 sum: 6
可以看到,我们在方法fn中打印出this.a,且把参数相加打印出来,我们来分析一下上面三个方法的使用和它们的打印结果。
首先先看看全局环境下使用fn方法,因为是在全局中调用的,不是使用new构造的,也并非硬绑定和软绑定,所以是默认绑定,在非严格模式下this.a指向了全局环境下的a,所以fn(1,2,3)最后得到的结果是a: 1 sum: 6。
好,知道了全局环境下直接使用该方法的结果后,我们来看看这三个方法,可以注意
到,三个方法的第一个参数都是对象obj,而最后打印出的结果正式obj.a的值,即是说,在第一个参数上,这三个方法都是一样的,是方法中this要绑定的对象。而后面的参数,是方法的参数,只是形式不一样而已。
我们可以看到,call和bind方法后面的参数都是以逗号分隔开的参数形式,而apply是以数组形式来存放参数的。这就是它们的第一个区别,即参数的传递形式。正是由于apply可以将数组内的成员作为函数的参数传递,所以我们很经常使用apply来把数组内的成员作为参数传递给一个方法。(ES6中提供了扩展运算符用于替代apply的这一用法)
var arr=[1,2,3]
function add(n1,n2,n3){
console.log(n1,n2,n3);
}
//apply的使用
add.apply(null,arr)//1 2 3
//扩展运算符
add(...arr)//1 2 3
在这里要提出apply的第二个参数实际上不一定要是一个数组,一个带有length的对象也是可以的
function add(n1,n2,n3){
console.log(n1,n2,n3);
}
add.apply(null,{length:3})
// undefined undefined undefined
//这里实际上可以将第二个参数看为[undefined,undefined,undefined]
接下来继续看三个方法的另一个区别,可以看到,bind后面多了一个括号,即call和apply返回的是函数的调用,而bind返回的是函数,需要再使用括号进行调用。既然如此,那原方法的参数是否可以放在后面的括号里呢,答案是可以的。
fn.bind(obj)(1, 2, 3);//a: 2 sum: 6
fn.bind(obj, 1)(2, 3);//a: 2 sum: 6
fn.bind(obj, 1, 2)(3);//a: 2 sum: 6
这里可以把bind看成是为部分参数提供默认值,利用bind的这一用法,可以很简便地实现函数的柯里化,当然,也可以用来实现thunk。
性能
除了用法之外,还有性能上的区别
这三者的性能排序从高到低(时间消耗从少到多)如下
- call
- bind
- apply
我写了一个demo结合performance来进行测试
<body>
<button onclick="_call()">call</button>
<button onclick="_apply()">apply</button>
<button onclick="_bind()">bind</button>
<script>
function fn(a, b) {
return a + b
}
function _call() {
for (let i = 0; i < 1000000; i++) {
fn.call(null, 1, 2)
}
}
function _apply() {
for (let i = 0; i < 1000000; i++) {
fn.apply(null, [1, 2])
}
}
function _bind() {
for (let i = 0; i < 1000000; i++) {
fn.bind(null)(1, 2)
}
}
</script>
</body>
然后在浏览器中,在不同的时间点点击按钮,查看performance中script的时间消耗,结果如图
可以明显看到,apply消耗的时间最多(黄色部分高度最高),接下来我们看到其中具体的时间来比较
这里call和bind消耗的时间差不多,实际上,call和bind在我测试的时候,有时call消耗时间多,有时bind消耗时间多,所以这两个性能上其实区别不大
但是我们可以看到,apply和这两个区别就大了,是十倍的时间,所以在一般情况下,我们尽可能地去使用call和bind,但是有一个问题,如果我们当前的参数在一个数组里,是不是直接用apply比较好呢,在以前,可能是的,但是在ES6之后,有了解构赋值,我们可以将数组的成员解构传参,这里我们修改一下_call函数,使用解构赋值,看性能上是否还是优于apply(因为call和bind差不多,所以我就只测试call做解构赋值处理了),修改函数如下
function _call() {
for (let i = 0; i < 1000000; i++) {
fn.call(null, ...[1, 2])
}
}
可以看到,时间的消耗比apply多,所以如果我们需要传入数组,还是以apply会比较好
总的来说,三个方法的区别就有三个
1.第一个参数后面的参数的形式不同,call和bind后面是逗号分隔的参数形式,apply后面是数组形式
2.返回的内容不同,call和apply返回函数调用,bind返回函数
3.性能上的区别,按各自用法call和bind的性能比apply好,但是结合解构赋值,apply性能更好
理解这三个方法的区别并在硬绑定时合理使用,会有很大的便利性。