清楚地理解 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.firstNameperson.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对象借用了appControlleravg()方法。在appController里的this的值将被设置为了gameController对象,因为我们将gameController对象作为第一个参数传递到了apply()方法。在apply()方法的第一个参数总是被显式地设置为this的值。

—— JSBin上的例子

后记

我希望你已经学会了足够帮你理解 JavaScript 中的 this 关键字了。现在,你已经有工具(bind, applycall 来设置this到一个对象)在必要攻克 JavaScript 中这些 this的棘手情况。

正如你已经学习了,在那些原始上下文(就是那些定义this的地方)改变时,this就变得有点麻烦了,尤其是在那些用在回调函数、当被不同的对象调用或者借用函数时。永远记住:this被赋予的值是那个调用this函数的对象的值。

好的,祝睡得安好和享受 Coding.