翻译自http://javascriptissexy.com/javascript-apply-call-and-bind-methods-are-essential-for-javascript-professionals/

要理解这篇文章,你需要提前阅读过下面这三篇文章:

如果你已经阅读过上面的文章的话,那么到目前为止,你应该知道,JavaScript里的函数也是对象。函数作为对象,有他的方法,包括强大的apply, call, bind这三个方法。一方面,applycall几乎是相同的,它们被大量用来明确指定this的指向。我们也会在可变参数函数上使用apply。我们马上就会讲解到。

另一方面,我们使用bind来设置this的值以及函数的柯里化。

我们将会在JavaScript里面使用这三个函数的每一个场景。我们需要知道,applycall是ECMAScript 3提出的方法,所以在低版本的浏览器上也可以使用,但是bind是ECMAScript 5提出的方法,只支持现代浏览器。这三个函数是JavaScript的主力,有的时候,你不可能不用到他们。我们先讲解bind函数。


JavaScript的bind方法

我们使用bind方法的主要目的是给this设置明确的值。换句话说就是,bind方法可以让我们决定在调用函数的时候将this绑定到哪个对象上。

可能这个作用看起来相对的显得微不足道,但是,当你真的需要一个明确的对象来绑定函数的this值的时候,就显示出他的重要性了。

当我们在某个方法使用this并且从另外一个对象上来调用这个方法的时候,我们就需要bind了。这种情况下,有时候this并没有绑定到我们预期要绑定的对象上从而导致应用出错。如果你不理解这个例子,不用担心,待会儿就会水落石出了。

在我们看本节的代码之前,应该要了解JavaScript的this。如果你不了解的话,可以参考这篇文章Understand JavaScript’s “this” With Clarity, and Master It。如果你没有好好理解this的话,那么,对于下面讨论的概念,你理解起来可能就有点困难。事实上,这篇文章里有关于设置this值的概念在上面的那篇文章里也有讲到。


bind方法允许我们在方法上设置this的值

下面这个例子中,当按钮被点击的时候,文本域里就会显示一个随机的名字。

1
2
<button>Get Random Person</button>
<input type='text'>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var user = {
data: [
{
name: 'T. Woods',
age: 37
}, {
name: 'P. Mickelson',
age: 43
}
],
clickHandler: function ( event ) {
var randomNum = ((Math.random() * 2 | 0) + 1) - 1; // random number between 0 and 1​
// 从data中添加一个随机的名字到文本域里面
$('input').val(this.data[randomNum].name + ' ' + this.data[randomNum].age);
}
};​
// 为鼠标的点击事件指定事件函数
$('button').click(user.clickHandler);

当点击按钮的时候会报错,因为clickHandler方法里的this被绑定到了按钮元素上,所以,按钮元素就是clickHandler执行的那个对象。

在JavaScript里,这是一个很普遍的现象。JavaScript的一些库比如jQuery和Backbone.js也会自动的执行this的绑定操作,以便于能够让this总是绑定在我们所期望的那个对象上。

要修复前面这个例子的问题,我们可以这样使用bind方法:

1
$('button').click(user.clickHandler.bind(user));

想想这个修复this的问题的方法:你将一个匿名函数传入click()方法,jQuery就会将this绑定到button对象上。

由于bind()方法是在ECMAScript 5中推出的,所以IE9以下以及Firefox3.x的浏览器就没法使用这个方法。如果需要兼容版本较老的浏览器,就把下面这段代码放到你的项目中吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if ( !Function.prototype.bind ) {
Function.prototype.bind = function ( oThis ) {
if ( typeof this !== 'function') {
throw new Error('What is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBound = this,
fNOP = function () {},
fBound = function () {
return fToBound.apply(
this instanceof fNOP ? this : oThis,
aArgs.concat(Array.prototype.slice.call(arguments))
)
};
if ( this.prototype ) {
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
}

我们继续上面的例子。如果我们将一个方法指派给一个变量,那么,this值也会绑定到另外的对象上。看下面这个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// data变量是一个全局变量
var data = [
{
name:'Samantha',
age:12
}, {
name:'Alexis',
age:14
}
];
var user = {
// 局部变量
data: [
{
name: 'T. Woods',
age: 37
}, {
name: 'P. Mickelson',
age: 43
}
],
showData: function ( event ) {
var randomNum = ((Math.random() * 2 | 0) + 1) - 1; // random number between 0 and 1​
console.log (this.data[randomNum].name + ' ' + this.data[randomNum].age);
}
};
// 将showData方法赋值给变量
var showDataVar = user.showData;
showDataVar(); // Samantha 12 (from the global data array, not from the local data array)​

当我们执行showDataVar的时候,控制台输出的值并不是来自于user对象的,而是来自于全局的data变量。为什么呢?因为showDataVar在执行的时候,就相当于是一个全局函数,它内部的this是绑定到了全局作用域的。

所以,我们再一次使用bind来修复这个问题:

1
2
3
4
5
// 将showData方法绑定到user对象​
var showDataVar = user.showData.bind(user);
// 现在从控制台输出的值就是来自于user对象的了
showDataVar(); // P. Mickelson 43​

bind()可以让我们借用方法

在JavaScript里,我们可以各处传递函数,返回函数,借用函数等等。使用bind方法可以让我们很轻松的去借用方法。

下面就是个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 下面这个cars对象不存在打印属性值到控制台的方法
var cars = {
data: [
{
name: 'Honda Accord',
age: 14
}, {
name:'Tesla Model S',
age: 2
}
]
};
// 我们可以从上个例子中的user对象中借用showData方法
// 我们将user.showData绑定到我们刚才创建的cars对象上
cars.showData = user.showData.bind(cars);
cars.showData(); // Honda Accord 14​

但是这样会存在一个问题,我们在隐形中为cars对象添加了一个新的方法,但是我们并不想在借用方法的时候这样做,因为cars对象上可能已经有了一个名为showData的方法或者属性了。我们不想因为巧合而把原来的方法给重写了。所以,我们会在下面讨论callapply,最好是使用它们二者之一来借用方法。


JavaScript的bind方法可以让我们对一个函数进行柯里化

函数柯里化,即函数的部分应用,在一个接受一个或者多个参数的函数中返回一个已经设定好了的一些变量的新的函数。这个返回的函数可以访问外部函数的arguments对象和变量。实际上没有听起来这么复杂。我们写一写代码就知道了。

我们使用bind()方法来进行函数的柯里化。首先,我们有一个接受三个参数的greet()函数:

1
2
3
4
5
6
7
8
9
10
11
function greet( gender, age, name ) {
// if a male, use Mr., else use Ms.​
var salutation = gender === 'male' ? 'Mr. ' : 'Ms. ';
if ( age > 25 ) {
return 'Hello, ' + salutation + name + '.';
}
else {
return 'Hey, ' + name + '.';
}
}

然后,我们使用bind()方法来对greet()进行函数的柯里化(预先设置一个或者多个参数)。与我们前面讨论的一样,bind()方法的第一个参数设置this值。

1
2
3
4
5
6
7
8
// 由于我们没有使用greet函数的this值, 所以我们传入null
var greetAnAdultMale = greet.bind(null, 'male', 45);
greetAnAdultMale('John Hartlove'); // 'Hello, Mr. John Hartlove.'
var greetAYoungster = greet.bind(null, '', 16);
greetAYoungster('Alex'); // 'Hey, Alex.'​
greetAYoungster('Emma Waterloo'); // 'Hey, Emma Waterloo.'​

当我们使用bind()方法对greet函数进行柯里化的时候,除了最后一个参数name,其他的参数都被预设值了。所以,当我们调用新的函数的时候,我们改变的是greet函数的最后一个参数name。另外,我在另外一篇博客里详细讲解了函数的柯里化。阅读那篇文章之后,你会很容易的使用柯里化和Compose这两个函数式概念来创建强大的函数。

所以,通过bind()方法,我们可以明确的设置调用方法的对象的this值,我们可以借用对象的方法,我们可以将函数赋值给变量,并且,还可以使用bind()来对函数进行柯里化。


JavaScript的apply()call()方法

在JavaScript里,callapply是使用的非常频繁的两个函数,他们也允许我们借用函数以及设置this值。另外,apply函数可以让我们使用数组作为参数来执行函数,这样,当函数执行的时候,每个参数都被独立的传入到函数里——也就是可变参数的函数。不像其他拥有固定参数的函数那样,可变参数的函数的参数是不固定的。

使用apply或者call来设置this

bind()的例子一样,我们也可以使用apply或者call来设置this值。这两个函数的第一个参数就是就是用来设置this值的。

在我们深入了解这两个函数之前,来快速的看一下下面这个简单的极具说明性的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 全局变量
var avgScore = 'global avgScore';
// 全局函数
function avg( arrayOfScores ) {
var sumOfScores = arrayOfScores.reduce(function ( prev, cur, index, array ) {
return prev + cur;
});
// 此处的this值将被绑定到全局对象, 除非我们使用call或者apply设置它的值
this.avgScore = sumOfScores / arrayOfScores.length;
}
var gameController = {
scores: [20, 34, 55, 46, 77],
avgScore: null
};
// 如果我们这样执行avg函数的话, this值就会被绑定到window对象
avg(gameController.scores);
console.log(window.avgScore); // 46.4​
console.log(gameController.avgScore); // null​
// 重新设置全局的avgScore
avgScore = 'global avgScore';
// 使用call来让this绑定到gameController
avg.call(gameController, gameController.scores);
console.log(window.avgScore); //global avgScore​
console.log(gameController.avgScore); // 46.4​

注意,call函数的第一个参数用来设置this的值。在前面的例子里,this被绑定到了gameController对象。剩下的参数都被作为avg函数的参数被传入了。

applycall方法在设置this值上几乎是相同的。不同在于传入apply的参数是一个数组,传入call的参数是单独的列出来。同时,apply函数拥有call函数没有的另外一个功能,我们马上就会看到。

使用call或者apply设置回调函数的this

我从我的另一篇文章借鉴了这一部分:Understand JavaScript Callback Functions and Use Them

1
2
3
4
5
6
7
8
// 定义一个有属性和方法的对象, 我们后面会将这个方法作为回调函数传入另一个函数
var clientData = {
id: 094545,
fullName: 'Not Set',
setUserName: function ( firstName, lastName ) {
this.fullName = firstName + ' ' + lastName;
}
};
1
2
3
4
function getUserInput( firstName, lastName, callback, callbackObj ) {
// 像下面这样使用apply可以将this值绑定到callbackObj
callback.apply(callbackObj, [firstName, lastName]);
}

applythis值绑定到了callbackObj。所以,执行回调函数的时候,传入回调函数的参数都会被设置在clientData对象上。

1
2
getUserInput('Barack', 'Obama', clientData.setUserName, clientData);
console.log(clientData.fullName); // Barack Obama​

在JavaScript 里,applycallbind方法都可以用来设置this值来允许我们直接控制以及其他的用途,只是它们各自的方式有些许的不同。与其它的语言类似,JavaScript的this值是很重要的一部分。在高效合适的设置this值上面,上述的三个方法都是必不可少的。

使用applycall来借用方法(必须了解的)

在JavaScript里,这两个函数用得最多的地方应该就是借用函数了。我们可以像使用bind那样来借用函数,但是,要比使用bind更加灵活多变。

看下面的例子:

借用数组方法

数组有很多的方法用来遍历和修饰数组,但不幸的是,对象却没有那么多的原生方法。但是,对象可以以某种方式来表现为数组,即类数组对象,并且,最重要的是,除了toStringtoLocalString之外,数组的其他方法都是属性方法,所以我们可以借用这些方法并且在类数组对象上使用。

所谓类数组对象,就是指有自己的非负数的整型键。由于对象上不存在length属性,但是数组上有这个属性,所以最好是在这个对象上添加一个表示这个对象长度的length属性。

对于JavaScript新手,应该要注意,在下面的例子中,当我们调用Array.prototype的时候,我们就深入了Array对象,并且,在他的原型上(即数组所有用于继承的方法定义的地方)。所以我们是从这个地方来借用方法的。所以类似于Array.prototype.slice这样的用法即slice方法是定义在数组的原型上。

我们先创建一个类数组对象,并且借用一些方法来操作我们这个类数组对象。记住,类数组对象是一个对象,完全不是一个数组。

1
2
3
4
5
6
7
var anArrayLikeObj = {
0: 'Martin',
1: 78,
2: 67,
3: ['Letta', 'Marieta', 'Pauline'],
length: 4
};

现在,如果想要使用数组的方法,我们可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 对对象进行复制并且将结果保存在真正的数组中
var newArray = Array.prototype.slice.call( anArrayLikeObj, 0 );
console.log (newArray); // ['Martin', 78, 67, Array[3]]​
// 在类数组对象中查找'Martin'
console.log(Array.prototype.indexOf.call(anArrayLikeObj, 'Martin') === -1 ? false : true); // true​
// 尝试在使用数组方法的时候不用apply或者call, 会报错
console.log(anArrayLikeObj.indexOf('Martin') === -1 ? false : true); // Error: Object has no method 'indexOf'​
// Reverse the object:​
console.log(Array.prototype.reverse.call(anArrayLikeObj));
// {0: Array[3], 1: 67, 2: 78, 3: 'Martin', length: 4}​
console.log(Array.prototype.pop.call(anArrayLikeObj));
console.log(anArrayLikeObj); // {0: Array[3], 1: 67, 2: 78, length: 3}​
console.log(Array.prototype.push.call(anArrayLikeObj, 'Jackie'));
console.log(aArrayLikeObj); // {0: Array[3], 1: 67, 2: 78, 3: 'Jackie', length: 4}​

当我们将对象设置为类数组对象并且借用数组方法的时候,我们可以拥有对象所拥有的优点,而且,还可以在对象上使用数组的方法。所有的这些,都是通过applycall的优点来实现的。

在JavaScript里,arguments对象是函数的属性,同时也是一个类数组对象,因此,有一个很流行的做法就是使用callapply来从arguments对象中选择需要传入的参数。

下面这个例子的代码是我从Ember.js里抽出来的,我添加了一些注释在上面。

1
2
3
4
5
6
7
8
9
10
function transitionTo( name ) {
// arguments对象是一个类数组对象, 所以我们可以在上面使用slice方法
// 数字1表示返回数组的除了第一个元素之外的元素
var args = Array.prototype.slice.call(arguments, 1);
console.log(args);
}
// 由于是从数组的索引1开始复制的, 所以数组的第一项没有返回
transitionTo('contact', 'Today', '20'); // ['Today', '20']​

args参数是一个真正的数组,它包含了传入transitionTo()函数的所有参数的一个复制。

从上面这个例子中,我们可以学到一种快速获得传入函数的参数的方法:

1
2
3
4
5
6
7
// 我们定义的这个函数没有任何参数, 但是我们可以得到所有传入这个函数的参数
function doSomething() {
var args = Array.prototype.slice.call(arguments);
console.log(args);
}
doSomething('Water', 'Salt', 'Glue'); // ['Water', 'Salt', 'Glue']​

我们在后面会再次更多的讲解对于变长参数的函数,如何在arguments对象上使用apply方法。

借用string类型的方法

与前面的例子一样,我们也使用applycall方法来借用字符串对象的方法。由于字符串是不变的,只有非操作性的数组方法才有效,所以不能使用reversepop等类似的方法。

借用其他的方法和函数

既然开始借用方法了,我们就继续深入,不仅仅是借用数组和字符串对象的方法,我们来借用我们自己定义的方法和函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var gameController = {
scores: [20, 34, 55, 46, 77],
avgScore: null,
players: [
{
name: 'Tommy',
playerID: 987,
age: 23
}, {
name: 'Pau',
playerID: 87,
age: 33
}
]
};
var appController = {
scores: [900, 845, 809, 950],
avgScore: null,
avg: function () {
var sumOfScores = this.scores.reduce(function ( prev, cur, index, array ) {
return prev + cur;
});
this.avgScore = sumOfScores / this.scores.length;
}
};
// 注意, 我们使用的是apply方法, 所以第二个参数是一个数组
appController.avg.apply(gameController);
console.log(gameController.avgScore); // 46.4​
// appController.avgScore依然是null值, 只有ganmeController.avgScore更新了
console.log(appController.avgScore); // null​

当然,上面这个例子很简单,但在借用我们自己定义的方法和函数这方面,是推荐的。gameController对象借用了appController对象的avg方法,在avg里面定义的this将被绑定到gemaController对象上。

你可能在思考,我们所借用的那个方法的原定义如果改变了的话会发生什么,这个借用的方法也会改变吗?或者说,借用的这个方法完全就是复制的,与原定义的方法没有一点关系?我们来看一个例子就知道了:

1
2
3
4
5
6
appController.maxNum = function () {
this.avgScore = Math.max.apply(null, this.scores);
};
appController.maxNum.apply(gameController, gameController.scores);
console.log (gameController.avgScore); // 77​

正如我们预料的一样,如果我们改变了原方法,这个改变也会反映到我们所借用的方法上面。这样的好处就是,我们不会完全复制这个方法,只是对这个方法的一个简单的借用。

使用apply来执行变参函数

为了更多的关注applycallbind的灵活性和用处,我们将讨论一个关于apply函数的简单的小功能:使用参数数组来执行函数。

我们可以向函数传入一个参数数组,通过apply函数,函数就会执行数组里的元素,就像我们下面这样调用一样:

1
createAccount(arrayOfItems[0], arrayOfItems[1], arrayOfItems[2], arrayOfItems[3]);

尤其是创建变参函数时候,就会用到这种技术。这种函数不像其他拥有固定参数数量的函数那样,他们可以接受任意数量的参数。函数的参数说明定义的函数可以接受的参数数量。

Math.max()就是一个很普通的变参函数:

1
2
// 我们可以传递任意数量的参数到这个函数里​
console.log(Math.max(23, 11, 34, 56)); // 56

但是,如果我们有一个数值数组要传入呢?当然,我们不能这样做:

1
2
var allNumbers = [23, 11, 34, 56];
console.log(Math.max(allNumbers)); // NaN

所以,这就是为什么我们要使用apply方法来帮我们执行变参函数了。使用apply,我们可以这样传入数值数组:

1
2
var allNumbers = [23, 11, 34, 56];
console.log(Math.max.apply(null, allNumbers)); // 56

正如我们之前学到的,apply的第一个参数用来设置this值,但是我们在Math.max中没有使用到this,所以传入null

下面这个我们自定义的变参函数的例子,更深入的说明了apply的这种能力:

1
2
3
4
5
6
7
8
9
10
11
var students = ['Peter Alexander', 'Michael Woodruff', 'Judy Archer', 'Malcolm Khan'];
function welcomeStudents() {
var args = Array.prototype.slice.call(arguments);
var lastItem = args.pop();
console.log('Welcome ' + args.join(', ') + ', and ' + lastItem + '.');
}
welcomeStudents.apply(null, students);
// Welcome Peter Alexander, Michael Woodruff, Judy Archer, and Malcolm Khan.

总结

对于设置函数的this值,创建并执行变参函数,借用方法和函数等,callapplybind这三个方法都是必不可少的,在你的JavaScript代码中,它们应该会有很大的作用。作为一个JavaScript开发人员,你很可能一次又一次的遇到并且使用这些方法,所以要好好理解他们。