JavaScript中的apply、call、bind方法

applycallbind方法都有改变函数的this的作用域,实际上等于设置函数体内的this对象的值。

apply

apply方法有两个参数,第一个参数为this所要指向的那个对象,第二个参数是一个数组,绑定对象的参数数组。apply()的参数为空时,默认调用全局对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
function add (x, y) {
console.log(x + y);
}
function multiply (x, y){
add.apply(this, [x, y]); //绑定参数组
}
function sub (x, y){
add.apply(this, arguments); //绑定arguments对象
}
multiply(2, 3); //5
sub(2, 3); //5

绑定arguments对象和绑定参数组在使用上没有区别,前者更常用。

call

call方法与apply方法作用相同,在参接上有所区别。第一个参数同样是this所要指向的那个对象,但是其余参数都是直接传递给函数。

1
2
3
4
5
6
7
8
9
function add (x, y, z) {
console.log(x + y + z);
}
function multiply (x, y, z){
add.call(this, x, y, z); //绑定参数列表
}
multiply(2, 3, 4); //9

bind

  • bind创建一个函数实例,参数传递形式与call方法相同。如果bind方法的第一个参数是nullundefined,等于将this绑定到全局对象,函数运行时this指向全局对象。
1
2
3
4
5
6
7
8
9
window.color = 'green';
var obj = {color:'red'};
function showColor (){
console.log(this.color);
}
showColor.call(window); //green
var objShowColor = showColor.bind(obj);
objShowColor(); //red

objShowColor方法是通过bind方法创建的showColor函数的实例方法,其this作用域为obj对象,因此,实列调用后输出值是“red”。

bind方法有一些使用注意点。

(1)每一次返回一个新函数

bind方法每运行一次,就返回一个新函数,这会产生一些问题。比如,监听事件的时候,不能写成下面这样。

1
element.addEventListener('click', o.m.bind(o));

上面代码中,click事件绑定bind方法生成的一个匿名函数。这样会导致无法取消绑定,所以,下面的代码是无效的。

1
2
3
element.addEventListener('click', o.m.bind(o)); //o.m.bind(o) `bind`方法生成的一个匿名函数
element.removeEventListener('click', o.m.bind(o)); //这里的o.m.bind(o) 是`bind`方法生成另一个新的匿名函数,所以removeEventListener不能取消绑定。

正确的方法是写成下面这样:

1
2
3
4
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);

(2)结合回调函数使用

回调函数是 JavaScript 最常用的模式之一,但是一个常见的错误是,将包含this的方法直接当作回调函数。解决方法就是使用bind方法,将counter.inc绑定counter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var counter = {
count: 0,
inc: function () {
'use strict';
this.count++;
}
};
function callIt(callback) {
callback();
}
callIt(counter.inc.bind(counter));
counter.count // 1

上面代码中,callIt方法会调用回调函数。这时如果直接把counter.inc传入,调用时counter.inc内部的this就会指向全局对象。

使用bind方法将counter.inc绑定counter以后,就不会有这个问题,this总是指向counter

还有一种情况比较隐蔽,就是某些数组方法可以接受一个函数当作参数。这些函数内部的this指向,很可能也会出错。

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
name: '张三',
times: [1, 2, 3],
print: function () {
this.times.forEach(function (n) {
console.log(this.name);
});
}
};
obj.print()
// 没有任何输出

上面代码中,obj.print内部this.timesthis是指向obj的,这个没有问题。但是,forEach方法的回调函数内部的this.name却是指向全局对象,

导致没有办法取到值。稍微改动一下,就可以看得更清楚。

1
2
3
4
5
6
7
8
9
10
obj.print = function () {
this.times.forEach(function (n) {
console.log(this === window);
});
};
obj.print()
// true
// true
// true

解决这个问题,也是通过bind方法绑定this

1
2
3
4
5
6
7
8
9
10
obj.print = function () {
this.times.forEach(function (n) {
console.log(this.name);
}.bind(this));
};
obj.print()
// 张三
// 张三
// 张三

(3)结合call方法使用

利用bind方法,可以改写一些 JavaScript 原生方法的使用形式,以数组的slice方法为例。

1
2
3
[1, 2, 3].slice(0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]

上面的代码中,数组的slice方法从[1, 2, 3]里面,按照指定位置和长度切分出另一个数组。这样做的本质是在[1, 2, 3]上面调用Array.prototype.slice方法,

因此可以用call方法表达这个过程,得到同样的结果。

call方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写。

1
2
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]

上面代码的含义就是,将Array.prototype.slice变成Function.prototype.call方法所在的对象,调用时就变成了Array.prototype.slice.call

类似的写法还可以用于其他数组方法。

1
2
3
4
5
6
7
8
9
var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);
var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]
pop(a)
a // [1, 2, 3]

如果再进一步,将Function.prototype.call方法绑定到Function.prototype.bind对象,就意味着bind的调用形式也可以被改写。

1
2
3
4
5
6
7
function f() {
console.log(this.v);
}
var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123

上面代码的含义就是,将Function.prototype.bind方法绑定在Function.prototype.call上面,所以bind方法就可以直接使用,不需要在函数实例上使用。

比较

callapply两个方法在作用上没有任何区别,不同的只是二者的参数的传递方式。至于使用哪一个方法,取决于你的需要,如果打算直接传入argumnets对象或应用的函数接收到的也是数组,
那么使用apply方法比较方变,其它情况使用call则相对方便一些。
bind方法会在指定对象的作用上创建一个函数实例,而call和apply方法是在指定对象的作用上运行函数。

bind方法会创建函数实例,所以需要运行实例后才会发生调用。而call和apply则会指定作用域上直接调用函数,不需要运行。