翻译自http://javascriptissexy.com/understand-javascripts-this-with-clarity-and-master-it/

JavaScript中的this关键词总是迷惑着新老程序员。本文的目的就是要把关于this的一切全部阐述清楚。读完本文之后,我们绝对不用再担心JavaScript中的this部分的知识了。我们会理解如何在各式各样的环境中正确的使用this,包括那些难以实现的棘手的环境。

我们使用 this的方式与我们在自然语言(比如英语或者法语)中使用代词的方式类似。我们会这样写一句话:“John is running fast because he is trying to catch the train.”。注意代词he的用法。我们完全可以这样写:“John is running fast because John is trying to catch the train.”。但是,我们不用重复的再使用一遍John,如果真这样的话,我们的家庭,朋友,同事啥的都会嫌弃我们的。好吧,也许你的家庭不会嫌弃你,但是那些陪你共患难的朋友同事可就不一定咯。在JavaScript中,我们也以这样的方式,把this作为代词或者说引用来使用;就是上下文的主体或者要执行的代码的主体。看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
var person = {
firstName: 'Penelope',
lastName: 'Barrymore',
fullName: function () {
/* 注意到我们在这里使用this就像前面使用he那样了吗? */
console.log(this.firstName + ' ' + this.lastName);
// 我们也可以这样写
console.log(person.firstName + ' ' + person.lastName);
}
};

如果我们在上面这个例子中使用person.firstNameperson.lastName,那么,这段代码就会有歧义了。想象一下,代码中可能有另外的一个叫做person的全局变量(我们可能知道也可能不知道),那么,对person.firstName的引用就可能是全局变量里的那个person,这样,就会导致很难调试的错误。所以我们使用this,并不只是为了看起来好看,而是为了明确代码的意义,让我们的代码看起来不那么含糊不清。就跟在之前的英语句子里使用代词he让句子结构更清晰一样。他能告诉我们我们引用的是句子开始的那个John

与使用he来代指前面的主语一样,JavaScript中的this也是用来引用函数所绑定的对象的。this关键词不仅仅引用了对象,还存储了这个对象的值。和代词一样,this可以作为上下文中的对象的简称。关于上下文,后面会讲到。


JavaScript中的this基础知识

首先要知道,JavaScript 中的所有函数都和对象一样都有属性。当函数执行的时候,他就获得了this这个属性——就是一个保存对象的值的变量,而这个对象正好调用了包含this的函数。

this总是引用并且保存一个单一对象的值,虽然它可以在全局作用域中使用,但是一般情况下都在函数或者方法体内使用。注意,当我们使用严格模式的时候,在全局函数中,this的值为undefined,在匿名函数中,他不会绑定任何的值。

this在函数(比如函数A)内部使用,并且保存了调用那个函数的对象的值。我们需要this来访问调用函数A的对象的属性和方法,尤其是当我们不知道这个对象的名称的时候,而且,有时候,也没有可用的名称来引用这个对象。事实上,this就是正在使用的对象的一个简称。

反复看看下面这个说明this用法的例子:

1
2
3
4
5
6
7
8
9
10
11
var person = {
firstName: 'Penelope',
lastName: 'Barrymore',
/* this是在showFullName里面使用的,而showFullName又是在person里面定义的,所以,person对象调用showFullName的时候,this就保存了person对象的值 */
showFullName: function () {
console.log(this.firstName + ' ' + this.lastName);
}
};
person.showFullName(); // Penelope Barrymore

再看看this在jQuery中的基本的用法:

1
2
3
4
5
$('button').click(function ( event ) {
// $(this) 将会保存按钮对象 ($('button')) 的值
// 因为按钮对象调用了click()函数
console.log($(this).prop('name'));
});

我先解释jQuery的这个例子:$(this)的用法是jQuery中使用this的语法。$(this)在匿名函数中使用,而这个匿名函数,又会在click()函数中执行。$(this)被绑定到了按钮对象的原因就是因为jQuery会把$(this)绑定到调用clcik()函数的对象上。因此,虽然$(this)是定义在一个匿名函数中,他自己不能访问外部函数的this变量,但是$(this)还是会保存$('button')对象的值。

注意按钮是HTML中的一个元素,同时也是一个对象。在这个例子中,因为他被包含在了$()中,所以它是一个jQuery对象。


this的最大要点

如果你理解关于this的这条定律,那么,对于this这个关键词,你就会很明了了。在对象调用包含this定义的那个函数的时候,this的值才被指定。我们把这个定义this的这个函数叫做this Function

很明显,虽然this引用了定义它的这个对象,但是并不是直到对象调用函数的时候this才被赋值。它的赋值仅仅是基于调用那个函数(this Function)的对象的。在大多数情况下,this保存的是要调用this Function的那个对象的值。但是,也有一些情况下,this没有能够保存那个对象的值。我后面会讲到那些情况。


在全局作用域下this的用法

在浏览器环境下执行代码的时候,所有的全局变量和全局函数都会被定义在window对象下面。因此,当我们在全局作用域下面使用this的时候,他所引用的值就是window对象(当然,肯定是在非严格模式下)。这个window对象也就是整个JavaScript应用或者说整个页面。

比如这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var firstName = 'Peter',
lastName = 'Ally';
function showFullName () {
/* 在这个函数里面的this保存的是window对象的值,因为showFullName是定义在全局作用域下的,就跟firstName和lastName一样 */
console.log(this.firstName + ' ' + this.lastName);
}
var person = {
firstName: 'Penelope',
lastName: 'Barrymore',
showFullName: function () {
/* 下面这行代码里面的this引用的是person对象,因为showFullName函数会被person对象调用 */
console.log(this.firstName + ' ' + this.lastName);
}
};
showFullName(); // Peter Ally​
/* 所有的全局函数和全局变量都定义在window对象下 */
window.showFullName(); // Peter Ally​
/* 在person对象里的showFullName方法里的this依然引用的是person对象 */
person.showFullName(); // Penelope Barrymore

this最令人误解和复杂的时候

this最令人费解的时候包括:

  • 借用一个使用了this的函数的时候
  • 使用this指定一个方法给变量的时候
  • 使用this的函数座位回调函数的时候
  • 在闭包里面使用this的时候(内部函数)

我们会复现每一个情况并且让this在每一个例子中都维持正确的值。

在继续之前,先说一下关于上下文一些知识

在JavaScript中的上下文与在英语句子中的主题类似:“John is the winner who returned the money.”。这个句子中的主语是John,我们可以说这个句子的上下文就是John,因为整个句子的中心都是围绕他的。甚至who这个代词也是指的John。正如我们在句子中使用分号来分隔句子的主题一样,我们也可以通过是用另一个对象来调用函数来把当前的上下文的切换到另一个对象。

类似的,在JavaScript的代码中就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var person = {
firstName: 'Penelope',
lastName: 'Barrymore',
showFullName: function () {
/* 当前的上下文 */
console.log(this.firstName + ' ' + this.lastName);
}
};
/* 当调用showFullName的时候, 上下文是person对象. 在showFullName方法里的this引用的是person对象的值 */
person.showFullName(); //Penelope Barrymore
/* 如果我们使用不同的对象调用showFullName */
var anotherPerson = {
firstName: 'Rohit',
lastName: 'Khan'
};
/* 我们可以使用apply方法来明确设置this的值(后面会更多的讲解apply方法) */
person.showFullName.apply(anotherPerson); //Rohit Khan​
/* 所以现在的上下文就是anotherPerson. 因为anotherPerson通过apply调用了showFullName方法 */

下面是this变得很复杂的一些情况。这些例子都包含了修复this值错误的方法。

1、当函数作为回调函数的时候

当我们把使用了this的方法作为回调函数传入的时候,事情就变得有点棘手了。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 我们想要当页面上的按钮点击的时候, 能够使用clickHandler方法 */
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​
/* 下面这行代码将会从数组中随机选择一个人的姓名和年龄进行打印 */
console.log (this.data[randomNum].name + ' ' + this.data[randomNum].age);
}
};
/* 按钮被$符号包含了起来,所以它是一个jQuery对象 */
/* 之所以会输出undefined是因为在button对象上没有数据属性 */
$('button').click(user.clickHandler); // Cannot read property '0' of undefined

在上面的代码中,由于$('button')是他自己的一个对象,我们把user.clickHandler作为回调函数传入了进去,所以,我们应该就知道,在user.clickHandler函数里的this引用的不再是user对象了。现在this引用的值是user.clickHandler所执行的那个对象。调用user.clickHandler的对象是按钮对象,这个函数将在按钮对象的click方法里执行。

注意,尽管我们调用clickHandler函数的时候使用的是user.clickHandler,但是,clickHandler函数自己会在按钮对象的上下文中执行。所以,现在this引用的是$('button')对象。

所以,现在很明显的是,什么时候上下文会发生改变:当我们在其他的对象上执行原来定义在对象上的方法的时候,this就不再引用原来所定义的this的对象了,他会引用调用这个函数的那个对象。

解决办法

既然我们确实想要this引用user对象,那么,我们可以使用bind(), apply(), call()方法来指定this的值。

我写过一篇关于这几个方法的详细的文章,包括如何在复杂的环境中使用他们来设置this的值。与其在这里长篇大论,倒不如推荐你把这篇文章阅读一下,我认为对于专业的JavaScript程序员来说,是有必要的。文章链接:JavaScript’s Apply, Call, and Bind Methods are Essential for JavaScript Professionals

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

原来的代码是这个样子:

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

使用bind()方法就会是这样:

1
$('button').click(user.clickHandler.bind(user)); // P. Mickelson 43

2、修复闭包中的this错误

另外一个容易引人误解的地方就是当我们使用闭包的时候。我们需要注意的很重要的一点就是闭包不能通过this这个关键词访问外部函数的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
var user = {
tournament: 'The Masters',
data: [
{
name: 'T. Woods',
age: 37
}, {
name: 'P. Mickelson',
age: 43
}
],
clickHandler: function () {
/* 在这里使用this没什么问题 */
this.data.forEach(function ( person ) {
/* 但是在匿名函数里面, this的指向就不再是user对象了. 这个内部函数不能访问外部函数的this */
console.log('What is This referring to? ' + this); //[object Window]​
console.log(person.name + ' is playing at ' + this.tournament);
// T. Woods is playing at undefined​
// P. Mickelson is playing at undefined​
})
}
};
user.clickHandler(); // What is 'this' referring to? [object Window]

在匿名函数中的this不能访问外部函数中的this,所以,他会被绑定到window对象下。

(译者加)注意: 嵌套函数,匿名函数,闭包,如果没有明确的对象进行调用,那么,它们的this指向全局对象,在浏览器中,即window

解决方案

为了要解决这个问题,我们可以使用一个很普通的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var user = {
tournament: 'The Masters',
data: [
{
name: 'T. Woods',
age: 37
}, {
name: 'P. Mickelson',
age: 43
}
],
clickHandler: function ( event ) {
/* 使用另外一个变量来存储user对象的this */
var theUserObj = this;
this.data.forEach (function ( person ) {
console.log (person.name + ' is playing at ' + theUserObj.tournament);
})
}
};
user.clickHandler();
// T. Woods is playing at The Masters​
// P. Mickelson is playing at The Masters

对于我来说,像下面那样,使用that来命名完全没有任何价值,而且使用起来一点不方便。所以,我会尽量把变量名写成能够描述this所引用的对象的名字,所以,在前面的代码中,就是这样了:var theUserObj = this

1
var that = this;

3、修复当把方法赋值给变量的时候的this的错误

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
32
33
34
35
36
/* 这个data是全局变量 */
var data = [
{
name:'Samantha',
age:12
}, {
name:'Alexis',
age:14
}
];
var user = {
/* 这个data是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);
}
};
/* 把user.showData赋值给变量 */
var showUserData = user.showData;
// When we execute the showUserData function, the values printed to the console are from the global data array, not from the data array in the user object​
/* 当执行showUserData函数的时候, 打印出来的信息是来自于全局变量data, 而不是user对象的data */
showUserData(); // Samantha 12

解决方案

我们可以使用bind()来指定this的值

1
2
3
4
5
/* 将showData方法绑定到user对象 */
var showUserData = user.showData.bind(user);
/* 现在我们输出的值就是来自于user对象的了 */
showUserData(); // P. Mickelson 43

4、修复当借用其他对象的方法时this的错误

在JavaScript开发中,借用方法是很普遍的,作为JavaScript程序员,毫无疑问我们会一次又一次的做这个事情。慢慢的,我们就会被这种节约时间的方式所吸引了。要想了解更多的关于借用方法的知识,参考JavaScript’s Apply, Call, and Bind Methods are Essential for JavaScript Professionals

我们来测试一下这个例子吧:

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
/* 我们有两个对象. 一个对象有avg方法,另一个没有. 所以我们需要借用这个方法 */
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;
}
};
/* 如果我们运行下面的代码, gameController.avgScore将被设置为appController的scores数组的平均值 */
/* 不要运行代码, 这只是举例而已. 我们想要的是appController.avgScore为null */
gameController.avgScore = appController.avg();

avg方法的this不会引用gameController对象,他还是会引用appController,因为是appController调用了他。

解决方案

要让this引用 gameController,我们需要使用apply()方法。

1
2
3
4
5
6
7
8
/* 注意我们使用的是apply方法, 所以第二个参数要是数组 */
appController.avg.apply(gameController, gameController.scores);
/* avgScore属性被成功设置在了gameController对象上, 尽管我们是从appController对象借用的这个方法 */
console.log(gameController.avgScore); // 46.4​
/* appController.avgScore属性仍然是null. */
console.log(appController.avgScore); // null

gameController对象借用了appController对象的avg方法,由于我们为apply函数传入的第一个参数为gameController,所以,this会被绑定到gameController对象上。apply函数的第一个参数就是用来明确this的指向的。


写在最后

我希望你已经足够了解JavaScript中的this关键字了。现在你又必须要征服的工具(bind(), apply(), call())。

正如你所学到的,this在下面这些情况中显得有点麻烦:

  • 原始上下文的改变,尤其是在回调函数中
  • 当使用不同的对象调用的时候
  • 借用方法的时候

但是永远记住,this总是引用调用this Function的那个对象。