如何理解JavaScript中的this

目录
文章目录隐藏
  1. this 关键词基础知识
  2. this 关键词使用误区
  3. 全局作用域下 this 的使用
  4. this 最容易用错的情况
  5. 使用 this 的方法被用作回调函数时
  6. 当方法作为回调函数时,让 this 获取正确值的方式
  7. this 被用于闭包时
  8. 在匿名函数里让 this 获取正确的值
  9. this 的方法被赋值给变量时
  10. 在方法被赋值给变量时让 this 获取正确的值
  11. 当使用 this 的方法被“借用”时
  12. 方法被借用时让 this 获取正确的值
  13. this 可以被 call/apply 改变
  14. 结束语

JavaScript 中的 this 对于初学者来说是个难点,对于老手也会困惑。之前有一个小伙伴一直问我 this 的相关问题,所以今天抽出点时间深入的带大家理解 this。希望通过我的理解能够对正在处于对 this 困惑的你指引方法,让你再也不用怕 JavaScript 中的 this 了,让你明白在各种情况下使用 this。JavaScript 的 this 关键词是很不一样,因为 JavaScript 本来就不是一门基于类的面向对象编程语言。this 就是一个指针,指向我们调用函数的对象

this 本身原本很简单,总是指向类的当前实例,this 不能赋值。这前提是说 this 不能脱离 类/对象 来说,也就是说 this 是面向对象语言里常见的一个关键字。说的极端点,如果你编写的 JS 采用函数式写法,而不是面向对象式,你所有的代码里 this 会少很多,甚至没有。记住这一点,当你使用 this 时,你应该是在使用对象/类 方式开发,否则 this 只是函数调用时的副作用。

this 关键词基础知识

首先你要知道 JavaScript 中所有的函数都有属性,就如对象有属性一样。函数执行时会获取 this 属性的值,此时 this 就是一个变量,储存着调用该函数的对象的值。

this 这个引用总是指代对象并储存着它的值(只能指代一个对象),一般都在函数或者对象方法里使用,但是也能用在函数外的全局作用域里。需要注意的是,如果在函数里使用严格模式,全局函数里 this 的值就是 undefined。而在匿名函数里则不会绑定任何对象。

假设在函数 A 里使用 this,它就储存着调用函数 A 的对象的值。要获取调用函数 A 的对象的属性和方法,就需要用到 this,特别是当我们不知道改对象的名称或者没有名称可以指代该对象。所以,需要用 this 作为一个快捷方式来指代“先行对象”,也就是调用函数的对象。

思考一下下面这段代码,它展示了如何在 JavaScript 中使用 this:

var person = {
    firstName   :"Penelope",
    lastName    :"Barrymore",
    //因为下方的 showFullName 方法使用了“this”关键词而且该方法是在 person 对象里定义的,
    //那么“this”就会储存着 person 对象的值,因为该对象会调用 showFullName()方法
    showFullName:function () {
    console.log (this.firstName + " " + this.lastName);
    }
    }
    person.showFullName (); // Penelope Barrymore

接着看一下下面很基础的 jQuery 范例代码,相信大家都见到过,这里面也用了 this 这个关键词:

$("button").click (function (event) {
    //$(this)会储存按钮对象($("button"))的值
    //因为 click()方法是由该按钮对象调用的
    console.log ($ (this).prop ("name"));
});

JavaScript this 关键词在 jQuery 里的语法形式是$(this),它被用在一个匿名函数里面,而该函数则在按钮对象的 click()方法里执行。$(this)会被绑定到按钮对象,是因为 jQuery 库将$(this)绑定到了调用 click 方法的对象中。所以尽管$(this)在匿名函数里定义且该函数本身无法访问外部函数的 this 变量,$(this)还是储存着 jQuery 按钮对象$(“button”)的值。

需要注意的是,该按钮不但是 HTML 页面的 DOM 元素,而且是一个对象。因为我们将它封装在 jQuery $()函数里,在这种情况下它就是一个 jQuery 对象。

this 关键词使用误区

只有当定义 this 的函数被对象调用时,this 才会被赋值。如果你理解这个 JavaScript 原则,那么你就能深刻地理解 this 关键词。我们暂且将定义 this 的函数称为“this 函数”。

尽管表面上看起来 this 指代的是定义它的对象,但只有当 THIS 函数被一个对象时,this 才会被赋值。该值完全取决于调用 THIS 函数的对象。在大部分情况下,this 储存的都是调用对象的值。然而少数情况下 this 储存的却不是调用对象的值,稍后我会讨论这些情况。

全局作用域下 this 的使用

当代码在浏览器里执行时,全局作用域里的所有全局变量和函数都在 window 对象里定义,所以在全局函数里使用 this,它指代 window 对象并储存着该对象的值(如上文提到的一样,严格模式除外)。window 对象是整个 JavaScript 程序或网页的主储存器。

var firstName = "Peter",
lastName = "Ally";
​
function showFullName () {
    //这个函数里的"this"会储存 window 对象的值
    //因为与变量 firstName 和 lastName 一样,showFullName()函数是在全局作用域里定义的
    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​
​
//showFullName()方法定义于 person 对象里,它里面的"this"依旧指代 person 对象,所以:
person.showFullName (); // Penelope Barrymore

this 最容易用错的情况

this 关键词在下列情况下最容易被用错:

  • 当使用 this 的方法被“借用”时;
  • 当使用 this 的方法被赋值给变量时;
  • 当使用 this 的方法被用作回调函数时;
  • 当 this 被用于闭包-内部函数里时。

下面我将通过代码例子一一探讨每种情况是如何发生的,同时给出让 this 获取正确值的方法。
函数可以在一个对象里定义并将其作为自己当前的上下文环境,也可以被其他对象调用,从而将上下文环境换成那个对象。

在 JavaScript 代码里可以做类似的事情:

var person = {
    firstName   :"Penelope",
    lastName    :"Barrymore",
    showFullName:function () {
        // “上下文环境”
        console.log (this.firstName + " " + this.lastName);
    }
}
​
//当调用在 person 对象上定义的 showFullName 方法时,其“上下文环境”就是 person 对象
//showFullName()方法里的 this 储存着 person 对象的值
person.showFullName (); // Penelope Barrymore​
​
//如果用其他对象来调用 showFullName()方法:
​var anotherPerson = {
    firstName   :"Rohit",
    lastName    :"Khan"​
};
​
//可以使用 apply 方法将 this 的设为特定的值 - 稍微会继续讨论 apply()方法
//无论哪个对象调用了 this,this 都会获取该对象的值,所以:
person.showFullName.apply (anotherPerson); // Rohit Khan​
​
//所以上下文环境现在就是 anotherPerson 对象,因为是它通过使用 apply()方法调用了 person.showFullName ()这个方法

总结:调用 this 函数的对象就是其上下文环境,但其他对象调用 this 函数就会变成其上下文环境。

使用 this 的方法被用作回调函数时

当使用 this 的方法作为回调函数传给其他函数时,这种情况就有点棘手。例如:

//创建一个包含 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; // 0 到 1 之间的随机整数
        //这行代码从 data 数组里随机获取名字(person's nam)和年龄(age)并输入
        console.log (this.data[randomNum].name + " " + this.data[randomNum].age);
    }
}
//button 被封装在 jQuery 的美元符封装器($)里,所以可以直接作为 jQuery 对象使用
//因为 button 对象没有 data 属性,所以结果为 undefined
$ ("button").click (user.clickHandler); //无法获取 undefined 名为“0”的属性

上面的代码中,按钮($(“button”))本身就是一个对象,我们将 user.clickHandler()方法作为其 click()方法的回调函数传入,所以 user.clickHandler()方法里的 this 将不再指代 user 对象。现在 this 指代的是执行 user.clickHandler()方法的对象。虽然 this 是在 user 对象里定义的,但是 button 对象调用了 user.clickHandler()方法 – 确切的说,user.clickHandler()方法是在 button 对象的 click()方法里被执行的。

需要注意的是,尽管我们是通过 user.clickHandler()这种形式(必须这样子做,因为 clickHandler()是在 user 对象里定义的方法)来调用 clickHandler () 方法的,clickHandler () 方法被执行时其上下文环境是 button 对象,所以 this 现在指代的是 button 对象($(“button”))。

说了这么多,应该很清楚了:当上下文环境改变时,也就是说方法在一个对象里定义却到另外一个对象里执行时,this 关键词不再指代原对象,而是指代调用该方法的对象。

当方法作为回调函数时,让 this 获取正确值的方式

如果要让 this.data 指代 user 对象的 data 属性,可以使用 Bind (),Apply ()或者 Call ()方法给 this 设置特定的值。

在我另一篇文章《JavaScript 的 Apply、Call 和 Bind 方法》里,详细地探讨了这些方法,并讲解了如何在各种容易出错的情况下使用他们正确设置 this 的值。这里就不重发一遍了。我觉得这篇文章作为 JavaScript 程序员都应该读一读,建议你好好看一下。

要解决前例的问题,可以使用 bind()方法,所以我们不这么写:

$("button").click (user.clickHandler);

而是这样子将 clickHandler()方法绑定到 user 对象:

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

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 是没问题的,因为“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(); // What is "this" referring to? [object Window]

匿名函数里的 this 无法访问外部函数的 this,所以在非严格模式下其被绑定了 window 对象上。

在匿名函数里让 this 获取正确的值

在匿名函数里使用 this,然后将函数传入为 forEach()方法的参数,会出问题。解决这个问题可以用 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

值得注意的是,很多程序员喜欢将变量命名为 that(见下方例子),然后将 this 赋值给它。个人觉得使用“that”这个词太过简单粗暴,我尽量让命名体现 this 指代的对象,所以我才在上面的代码中使用 theUserObj = this。

// 程序员的常用手法
var that = 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; // 0 到 1 之间的随机整数
    ​
        //这行代码由 data 数组里产生一个随机的 person 数据加到 text 字段里
        console.log (this.data[randomNum].name + " " + this.data[randomNum].age);
    }
}
​
//将 user.showData 赋值给变量
var showUserData = user.showData;
​
//当执行 showUserData()函数时,控制台输出的值来自全局的 data 数组,而不是 user 对象的 data 数组
showUserData (); // Samantha 12 (来自全局的 data 数组)​

在方法被赋值给变量时让 this 获取正确的值

我们可以用 bind()方法设置 this 的值来解决问题:

//将 showData 方法绑定到 user 对象上
var showUserData = user.showData.bind (user);
​
//因为 this 关键词绑定到了 user 对象上,现在得到的是 user 对象的值
showUserData (); // P. Mickelson 43

当使用 this 的方法被“借用”时

在 JavaScript 开发里借用其他对象的方法是很常见的行为,作为 JavaScript 开发者当然时不时会这样子做,以此来节省开发时间。我在另外一篇文章里深入剖析了如何借用其他对象的方法:《JavaScript 的 Apply、Call 和 Bind 方法》。

我们来看下方法被借用后,this 是如何指代对象:

//这里有两个对象,一个有叫 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 对象,this 可以被 call/apply 改变,所以我使用 apply ()方法:

//注意使用的是 apply()方法,所以第二个参数必须是数组-这些参数最后都会被传给 appController.avg()方法
appController.avg.apply (gameController, gameController.scores);
​
//gameController 对象上的 avgScore 属性成功设置,尽管 avg()方法是从 appController 对象借过来的
console.log (gameController.avgScore); // 46.4​
​
//appController.avgScore 依然未 null,其值没有被更新,只有 gameController.avgScore 的值被更新了
console.log (appController.avgScore); // null

avg 方法的 this 关键词不会指代 gameController 对象,而是指代 appController 对象,因为调用它的是 appController 对象。

this 可以被 call/apply 改变

call()/apply() 是函数调用的另外两种方式,两者的第一个参数都可以改变函数的上下文 this。call/apply 是 JS 里动态语言特性的表征。动态语言通俗的定义 程序在运行时可以改变其结构,新的函数可以被引进,已有的函数可以被删除,即程序在运行时可以发生结构上的变化。

var m1 = {
    message: 'This is A'
} 
var m2 = {
    message: 'This is B'
}  
 
function showMsg() {
    alert(this.message)
}
 
showMsg() // undefined
showMsg.call(m1) // 'This is A'
showMsg.call(m2) // 'This is B'

可以看到单独调用 showMsg 返回的是 undefined,只有将它绑定到具有 message 属性的对象上执行时才有意义。发挥想象力延伸下,如果把一些通用函数写好,可以任意绑定在多个类的原型上,这样动态的给类添加了一些方法,还节省了代码。这是一种强大的功能,也是动态语言的强表现力的体现。

结束语

函数的上下文 this 是 JS 里不太好理解的,在于 JS 函数自身有多种用途。目的是实现各种语言范型(面向对象,函数式,动态)。this 本质是和面向对象联系的,和写类,对象关联一起的, 和“函数式”没有关系的。如果你采用过程式函数式开发,完全不会用到一个 this。 但在浏览器端开发时却无可避免的会用到 this,这是因为浏览器对象模型(DOM)本身采用面向对象方式开发,Tag 实现为一个个的类,类的方法自然会引用类的其它方法,引用方式必然是用 this。当你给 DOM 对象添加事件时,回调函数里引用该对象就只能用 this 了。this 的上下文环境一改变,有时就会很麻烦。当涉及到回调函数,或者方法由其他对象调用,又或者方法被其他对象借用时,会特别绕。反正你记住 this 储存的是调用 THIS 函数的对象的值就行了。

「点点赞赏,手留余香」

3

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » 如何理解JavaScript中的this

3 评论

  1. 学习了,this确实是个难点

    1. 可以,写的不错

回复 码云 取消回复