[翻译]清楚地理解JavaScript中的this并且掌握它
Contents
清楚地理解 JavaScript 中的 this, 并且掌握它
原文:Understand JavaScript’s “this” With Clarity, and Master It
(当
this
让最你棘手时,可以学习以下所有使用this
的情景)
前提:一点 JavaScript 基础 学习时间:大约 40 分钟
在 JavaScript 中this
关键字同样困惑着 JavaScript 新手以及经验丰富的开发者。本文的目标就是完整地解释this
。当我们学习完这篇文章后,JavaScript 中的this
关键字这部分 我们就不必再担心了。我们将会明白如何在每个脚本里正确地使用this
,包括那些难以捉摸和棘手情况的地方。
我们使用this
,类似于我们在自然语言比如英语和法语中的代词。我们写 :“John 正在飞快地跑着,因为他正在试图追上火车“。
注意代词 他
的使用。我们可以写成这样子:“John 正在飞快地跑着,因为 John 正在试图追上火车”。我们不要以这种方式重复地使用John
,如果我们这样子做了,我们的家人、朋友和同事会很嫌弃我们。是的,他们肯定会这样子。好吧,也许不是你的家人,而是我们的那些酒肉朋友和同事。以类似的优雅方式,在 JavaScript 中, 我们使用this
关键字作为一个快捷方式,一个引用;它引用一个对象;也就是在上下文中的主语,或者是正在执行代码的主体。思考下下面的例子:
var person = {
firstName: "Penelope",
lastName: "Barrymore",
fullName: function () {
// 注意我们使用`this`就像我们上面例句中使用的`他` :
console.log(this.firstName + " " + this.lastName);
// 我们也可以写成这样子:
console.log(person.firstName + " " + person.lastName);
}
}
如果我们使用person.firstName
和person.lastName
,在上面的例子中,我们的代码会变得含糊。考虑下有另一个名为person
的全局变量(我们可能或根本就没有意识到)。然后,引用 person.firstName
,可能试图访问这个person
的全局变量的firstName
属性,并且会导致难以调试的错误。所以,我们使用this
关键字不仅为了美学(例如,作为一个指示词),更为了准确;它的使用实际上使我们的代码更加明确,正如代词他
使我们的句子更加清晰一样。它告诉我们:我们正在引用的是在句子开头特定的John
。
就像代词他
是用于指示先行词(先行词是代词指示的名词),this
关键字类似地用于指示一个函数(this
就是被用在这里)所绑定的对象。this
关键字不仅是引用一个对象,并且它也包含对象的值。就像代词,this
可以被用作一个快捷方式(或者一个相当明确的替代)来引用之前在上下文中的对象(“先行词对象”)。我们迟些将学习更多关于上下文的知识。
## JavaScript的this
关键字的基础知识
首先,我们知道 JavaScript 中的所有函数都有属性,就像对象有属性一样。并且,当一个函数执行的时候,它就获得了this
属性 ———— this
是用于一个带有调用函数的对象的值的变量。this
引用总是指示(并且持有值)一个对象 ———— 一个单一对象 ———— 并且它通常用在一个函数或者方法中,尽管它也可以用于全局作用域的函数外部。注意,当我们使用strict mode
模式时,this
在全局函数中持有undefined
的值,并且在匿名函数里并没有绑定任何对象。
译者注:可以用下面的例子来演示:
"use strict";
console.info(this);
(function() {console.info(this) }())
function hello(){
console.info(this);
}
hello()
输出的结果为:
[Running] node "/var/folders/lz/mpwxkp6s7rq62r08vt2h_j480000gn/T/temp-sgwrfzpgrf.javascript"
{}
undefined
undefined
[Done] exited with code=0 in 0.137 seconds
this
用于函数内部(让我们假设是函数 A)并且它包含了调用函数 A 的对象的值。我们需要this
来访问调用函数 A 的对象的方法和属性,特别地,由于我们并不总是知道调用对象的名字,而且有时是没有名字来引用调用者对象的。确实,this
实际上仅仅是一个快捷引用“先行词对象”————调用者对象。
反复思考一下以下这个在 JavaScript 中使用 this
的基本例子:
var person = {
firstName :"Penelope",
lastName :"Barrymore",
// `this` 关键字用在 showFullName 方法里,并且 showFullName 方法是定义在 person 对象上,
// `this` 关键字就会拥有 person 对象的值,因为 person 对象将会调用 showFullName()
showFullName:function () {
console.log (this.firstName + " " + this.lastName);
}
}
person.showFullName (); // Penelope Barrymore
并且再思考下 jQuery 中 this 的基础用法例子:
// jQuery 中常见的代码片段
$ ("button").click (function (event) {
// $(this) 将拥有 button ($("button"))对象的值
// 因为 button 对象调用 click
console.log ($ (this).prop ("name"));
});
我将详解前面的 jQuery的例子:$(this)
的使用,它是 JavaScript 中的 this
关键字在 jQuery 中的语法,用在一个匿名函数里面,并且匿名函数是被 button 的 click() 函数执行。$(this)
被绑定到 button 对象的原因,是由于 jQuery 库绑定了 $(this)
到那些调用 click 方法的对象身上。因此,$(this)
将拥有 jQuery button ($("button")
)对象的值,尽管 $(this)
是被定义在一个匿名函数的内部,在匿名函数的外部,它是不能访问自身的this
变量的。
注意:button 是一个在 HTML 页面的 DOM 元素,并且它也是一个对象;在这情况下,它是一个 jQuery 对象,因为我们在 jQuery $() 函数包住了它。
更新:下面(“最大的问题”部分)是在我发布了这篇文章几天后添加的。
JavaScript this
关键字的最大问题
如果你理解了这一个 JavaScript中的 this
的原则,你将会清晰地明白this
关键字了:this
是直到一个对象调用了函数才会在this
定义的地方进行赋值。让我们将那些定义this
的函数的地方叫作“this 函数”。
尽管this
的出现是指示定义它的对象,它是直到一个对象调用this 函数
时才会真正地赋值的。并且这值是完全基于那个调用this 函数
的对象的。在绝大部分情况下,this
有一个调用对象。然而,有极少情况下,this
是没有调用者对象的值的。迟些会接触到这些情况。
在全局作用域范围中使用this
在全局作用域范围,当代码正在浏览器中执行时,所有全局变量和函数都被定义在一个window
对象上。因此,当我们在全局函数中使用this
时,它是引用(并且拥有值)全局的window
对象(不是在strict
模式,这个在前面提到过了),它是整个 JavaScript 应用程序或者web 页面的主要容器。
这样:
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
// `this`定义在 person 对象的 showFullName() 方法内部,所以仍然引用的是 person 对象,因此:
person.showFullName (); // Penelope Barrymore
当 this
最容易曲解以及变得棘手时
当我们通过方法来使用this
时、当我们通过this
将一个方法赋值到一个变量时、当一个函数使用回调(callback)函数使用 this
时、以及当this
被用在一个闭包里面———— 一个内部函数时, this
关键字是最容易曲解的。我们将在每一个例子中看一下在这些情景中的this
代表适当的值的解决办法。
重点注意:
在我们继续之前,了解一下上下文(context)
在 JavaScript 中,上下文类似于英语中句子的主体:“John is the winner who returned the money.”。 这个句子中的主体就是John
,我们可以说句子的上下文(context)就是John
,因为在此时此句中,焦点就是他。并且像我们可以使用分号来切换句子的主体,我们可以通过另一个对象来调用函数从当前上下文的对象来切换到另一个上下文的对象。
类似地,在 JavaScript 代码中:
var person = {
firstName :"Penelope",
lastName :"Barrymore",
showFullName:function () {
// 这就是 上下文(context)
console.log (this.firstName + " " + this.lastName);
}
}
// 当我们正调用 person 对象的 showFullName() 方法时, 上下文就是 person 对象。
// 并且,`this`在 showFullName() 方法中的使用,它拥有 person 对象的值。
person.showFullName (); // Penelope Barrymore
// 如果我们用另一个不同的对象来调用 showFullName
var anotherPerson = {
firstName :"Rohit",
lastName :"Khan"
};
// 我们可以显式地使用`apply`方法来设置`this`的值 —— 后面会有更多关于 `apply()` 方法的介绍
// 这时,`this` 获得了无论是哪个对象调用了`this 函数`的值 ,因此:
person.showFullName.apply (anotherPerson); // Rohit Khan
// 所以,现在的上下文(context)就是 anotherPerson 了,因为 anotherPerson 通过使用`apply()`方法来调用了 person.showFullName() 方法时,
外带的是,在上下文中对象调用this 函数
,我们可以通过另一个对象来调用this 函数
来改变上下文;然后,这新的对象就在上下文中了。
以下是一些this
关键字变得棘手的情景,他们包含了修正this
的错误的例子。
1. 当用在回调函数时,this 的困境
当我们传递一个方法(使用this
的方法)作为一个参数用作一个回调函数来使用时,这会变得比较棘手。例如:
// 假设我们有一个带有当在页面上的 button 被点击时就调用 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
// 这一行是随机从 data 数组中打印用户对象的名字和年龄
console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
}
}
// button 现在包装在 jQuery 的 $ 符号里,所以,它现在是一个 jQuery 对象
// 并且输出将会是 `undefined`, 因为在 button 对象中,并没有 data 属性。
$("button").click(user.clickHandler); // 并不能读取 `undefined` 的'0' 属性
在以上代码中,由于 button ($("button"))
自己是一个对象,并且我们传递user.clickHandler
方法作为它的click()
方法的回调函数,我们知道this
是在user.clickHandler
方法内部,它将不再引用user
对象了。this
现在引用的对象是user.clickHandler
方法被执行的对象了,因为this
是定义在user.clickHandler
方法的内部。既然正在调用user.clickHandler
方法的对象是 button 对象 ———— user.clickHandler
将被执行在 button 对象的 click 方法内部。
注意,尽管我们正在调用的clickHandler()
方法是user.clickHandler
(这是我们必须这样子做的,因为 clickHandler
是定义在 user 内部),clickHandler
方法自身将被现在上下文中的this
———— button 对象调用。
在这一点上,很明显的是当上下文改变时 ———— 当我们执行一个不是在对象自身定义而是在其他对象上定义的方法时,this
关键字就不再引用定义this
的自身对象了,而是引用了调用这些定义this
方法的对象。
当一个方法是作为一个回调函数来传递的this
解决方案
我们真正地想让 this.data
是引用 user 对象的 data 属性时,我们可以使用bind(), apply() 或 call()
方法来显式地设置this
的值。
我已经写了一篇详细的文章:JavaScript’s Apply, Call, and Bind Methods are Essential for JavaScript Professionals,在这些方法里,包括如何在各种棘手的情景下使用它们来设置this
的值的。为了不重复发布这些细节到这里,我建议你先完整阅读那篇文章先,这是一篇我认为对于一名 JavaScript 专业人士必读的文章。
为了修正前面例子的问题,我们可以使用bind
方法,因此:
替代这一行:
$ ("button").click (user.clickHandler);
我们必须bind
绑定clickHandler
方法到 user 对象,就像这样:
$("button").click(user.clickHandler.bind(user)); // P. Mickelson 43
—— 在JSBin上看一下一个可行的例子
2. 在闭包中时, this 困境
另一个比较棘手的例子是,我们把this
用在一个内部函数(闭包)中。有一点是非常重要的是要注意:闭包并不能通过使用this
关键字来访问外部函数的this
变量,因为this
仅仅只能被函数自身访问,而不是内部函数。例如:
var user = {
tournament: "The Masters",
data: [
{
name: "T. Woods",
age: 37
},
{
name: "P. Mickelson",
age: 43
}
],
clickHandler: function () {
// 在这里使用 `this.data`是 OK 的,因为`this`是引用 user 对象,并且 data 是 user 对象的一个属性。
this.data.forEach(function (person) {
// 但是,在这个匿名函数的内部(传递到 forEach 参数的方法),`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(); // 这时`this`引用的对象是哪个?—— `window` 对象
this
在匿名函数的内部并不能访问外部函数的this
,所以,它(匿名函数的this
)绑定到了全局作用域的window
对象,当没有使用strict
模式时。
this
作为内部匿名函数里使用的解决方案
为了解决在传递给forEach
方法参数的匿名函数中使用this
的问题,我们使用一个在 JavaScript 里常用实践:在我们进入 forEach
方法之前, 设置this
的值到另一个变量里:
var user = {
tournament: "The Masters",
data: [
{
name: "T. Woods",
age: 37
},
{
name: "P. Mickelson",
age: 43
}
],
clickHandler: function (event) {
// 为了获取`this`是指向 user 对象的值,我们必须将它赋值到另一个变量里
// 我们设置`this`的值到 theUserObj 变量里了,所以我们可以在后面使用它
var theUserObj = this;
this.data.forEach(function (person) {
// 替代 this.tournament, 我们现在使用 theUserObj.tournament
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
值得注意的是,许多 JavaScript 开发者喜欢使用名为that
的变量名,正如下面所见的一样,来保存this
的值。使用that
这个词对我来说是非常尴尬的,所以,我试图命名这个变量为一个描述this
真正要引用的对象的名词,所以,在上面的代码里,我这里使用了 var theUserObj = this
。
// 常见于在 JavaScript 开发者里使用下面的代码
var that = this;
—— JSBin上的例子
3. 当一个方法被赋值到一个变量时, this 的困境
如果我们赋值一个使用this
的方法时,this
的值会逃逸我们的想像并且它被绑定到另一个对象。让我们看以下的代码:
// 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
// 这行是从 data 数组中添加一个 person 对象为文本字符串表示
console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
}
}
// 将 user.showData 赋值到一个变量 showUserData
var showUserData = user.showData;
// 当我们执行 showUserData 函数,打印出的值将会是全局变量的 data 中的值,而不是 user 对象中的 data 数组的值
//
showUserData(); // Samantha 12 (from the global data array)
当一个方法被赋值到一个变量时的解决方案
我们可以通过显式地通过bind
方法来设置this
的值来解决这个问题:
// 绑定 showData() 方法到 user 对象
var showUserData = user.showData.bind (user);
// 现在,我们获取了 user 对象的值,因为`this`关键字绑定到了 user 对象上
showUserData (); // P. Mickelson 43
4. 当借用方法时, this 的困境
在 JavaScript 开发中,借用方法是一个常见的用法,并且作为 JavaScript 开发者,我们无疑会一次又一次地遇到这种做法。并且时不时地,我们也将参与这种节省时间的实践中。关于更多的借用方法的细节,请阅读我的深入的文章 JavaScript’s Apply, Call, and Bind Methods are Essential for JavaScript Professionals。
让我们仔细检查一下在借用方法相关的上下文中的 this
:
// 假设我们有两个对象。一个对象有名为 avg() 方法,而另一个对象则没有该方法。
// 所以,我们将借用 (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 的解决方案
为了解决这个问题并且确定在 appController.avg()
方法内的this
是引用gameController
,我们可以使用apply()
方法,因此:
// 注意,我们正使用 apply() 方法,所以第二个参数必须是一个数组 —— 这个参数是传递到 appController.avg() 方法的。
appController.avg.apply (gameController, gameController.scores);
// avgScore 属性现在被正确地设置到了 gameController 对象了,尽管我们是从 appController 对象上借用了 avg() 方法
console.log (gameController.avgScore); // 46.4
// appController.avgScore 仍然是 null,它没有被更新,仅仅是 gameController.avgScore 被更新了
console.log (appController.avgScore); // null
gameController
对象借用了appController
的avg()
方法。在appController
里的this
的值将被设置为了gameController
对象,因为我们将gameController
对象作为第一个参数传递到了apply()
方法。在apply()
方法的第一个参数总是被显式地设置为this
的值。
—— JSBin上的例子
后记
我希望你已经学会了足够帮你理解 JavaScript 中的 this
关键字了。现在,你已经有工具(bind
, apply
和 call
来设置this
到一个对象)在必要攻克 JavaScript 中这些 this
的棘手情况。
正如你已经学习了,在那些原始上下文(就是那些定义this
的地方)改变时,this
就变得有点麻烦了,尤其是在那些用在回调函数、当被不同的对象调用或者借用函数时。永远记住:this
被赋予的值是那个调用this函数
的对象的值。
好的,祝睡得安好和享受 Coding.