精通JavaScript的this关键字


Posted in Javascript onMay 28, 2020

JS中的this关键字让很多新老JS开发人员都感到困惑。这篇文章将对this关键字进行完整地阐述。读完本文以后,您的困惑将全部消除。您将学会如何在各种不同的情形正确运用this。

我们和在英语、法语这样的自然语言中使用名词一样地使用this。比如,“John飞快地跑着,因为他想追上火车”。请注意这句话中的代指John的代名词“他”。我们原本也可以这样表达,“John飞快地跑着,因为John想追上火车”。按照正常的语言习惯,我们并不按第二种方式表达。如果我们真按第二种方式说话,我们的家人和基友一定会把我们当成怪胎。说不定不止家人,甚至连我们的酒肉朋友和同事都会远离我们。类似地,在JS中,我们把this关键字当成一种快捷方式,或者说是引用(referent)。this关键字指向的是当前上下文(context,下文中将会对此作专门的解释)的主体(subject),或者当前正在被执行的代码块的主体。

考虑以下代码:

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属性(property)。这样一来,我们调试代码的时候很难发现错误。所以this并不只起到美化代码的作用,同时也是为了保证程序的准确性。这种做法实际上和前面讲到的“他”的用法一样,使得我们的代码更加清晰。“他”所引用的正是句首的“John”。

正如代名词“他”被用来代指句中的先行词(先行词就是代名词所指示的名词),this关键字以同样的方式来引用当前方法(function,也可以称为函数)所绑定(bound)的对象。this不只引用对象,同时包含了对象的值。跟先行词一样,this也可以被理解成在上下文中用来引用当前对象(又叫作“先行词对象”)的快捷方式(或者来适度减少歧义的替代品)。我们迟些会专门讲解“上下文”。

this关键字基本理论

首先我们得知道,对象(Object)有属性集(properties),所有的方法(function)也有属性集。运行到某个方法的时候就有了一个this属性—一个存储了调用该方法(准确地说是使用了this关键字的方法)的对象的值的变量。

this关键字始终指向一个对象并持有这个对象的值,尽管它可以出现在全局范围(global scope)方法(函数)以外的地方,但它通常出现在方法体中。值得注意的是,如果我们使用严格模式(strict mode),并在全局方法(global functions)或者没有绑定到任何对象的匿名方法中使用this关键字时,this将会指向undefined。

this被用在方法体中,比如方法A,它将指向调用方法A的对象的值。并不是任何情况我们都能找到调用方法A的对象名,这时候就用this来访问调用方法A的对象所拥有的方法和属性。this确实只是一个用来引用先行词—调用方法的对象—的快捷方式。

我们来仔细体会下面这段使用this的代码。

var person = {
 firstName: "Penelope",
 lastName: "Barrymore",
 //this用在showFullName方法中,而showFullName定义在person对象中,由于调用showFullName的是person这个对象,所以this拥有person的值

 showFullName: function() {
  console.log(this.firstName + " " + this.lastName);
 }
}
person.showFullName(); // 结果:Penelope Barrymore

再考虑下面这段使用了this的jQuery示例。

//这是一段很简单的jQuery代码

$("button").click(function(event) {
 // $(this) 会指向$("button")对象
 // 因为$("button")对象调用click方法
 console.log($(this).prop("name"));
});

我想详细地说一下上面这个jQuery示例:$(this)的使用,这是this的jQuery版本,它用于匿名方法中,这个匿名方法在button的单击事件里执行。这里之所以说$(this)被绑定到button对象,是因为jQuery库把$(this)绑定到调用click方法的对象上。因此,尽管$(this)被定义在一个自身无法访问“自身”变量的匿名方法里,$(this)仍会指向button对象。

请注意,button是一个HTML页面的DOM元素,它同时是一个对象:在上面的例子中,因为我们把它包装在了jQuery的$()方法里,所以它是一个jQuery对象。

this关键字的核心

下面这条规则可以帮助你彻底搞懂this关键字:如果一个方法内部使用了this关键字,仅当对象调用该方法时this关键字才会被赋值。我们估且把使用了this关键字的方法称为“this方法”。

尽管看上去this引用了它在代码中所存在于的对向,事实上在方法被调用之前它并没有被赋值,而赋给它的值又严格地依赖于实际调用“this方法”的对象。this通常会被赋予调用对象的值,下面有一些特殊情况。

全局范围里的this

在全局域中,代码在浏览器里执行,所有变量和方法都属于window对象。因而当我们在全局域中使用this关键字的时候,它会被指向(并拥有)全局变量window对象。如前所述,严格模式除外。window对象是JS一个程序或一张网页的主容器。

因而:

var firstName = "Peter",
lastName = "Ally";

function showFullName() {
 //在这个方法中,this将指向window对象。因为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

//使用了this关键字的showFullName方法定义在person对象里,this关键字指向person对象,因以会有以下输出
person.showFullName(); // Penelope Barrymore

对付this有绝招

当方法内使用了this关键字时,这几种情况最容易引起误解:方法被借用;把方法赋值给某个变量;方法被用作回调函数(callback),被作为参数传递;this所在的方法是个闭包(该方法是一个内部方法)。针对这几种情况,我们将逐一攻破。在此之前,我们先简单介绍一下“上下文”(context)。

JS当中的上下文跟这句话中的主语(subject)类似:“John是赢家,他还了钱”。这句话的主语是John。我们也可以说这句话的上下文是John,因为我们在这句话中关注的是John,即使这里有一个“他”字来代指John这个先行词。正如我们可以使用分号来切换句子的主语一样,通过使用不同的对象来对方法进行调用,当前的上下文对象同样可以被切换。

类似地,以下JS代码:

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,因为anotherPerson通过借助使用apply方法间接调用了person的showFullName方法

现在我们开始正式讨论应付this关键字的绝招,例子里包含了this所引发的错误以及解决方案。

1.当this被用作回调函数传入其它方法

当我们把一个使用了this关键字的方法当成参数作为回函数的时候,麻烦就来了。例如:

//以下是一个简单的对象,我们定义了一个clickHandler方法。我们想让这个方法在页面上某个button被单击时执行。
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里随机打印姓名和年龄
  console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
 }
}

//button对象被jQuery的$方法包装,现在变成一个jQuery对象
//所以输出结果是undefined,因为button对象没有data这个属性
$("button").click(user.clickHandler); // 无法读取未定义的属性

上面的代码中,我们把user.clickHandler当成回调函数传入$(“button”)对象的click事件,user.clickHandler中的this将不再指向user对象转。谁调用了这个包含this的方法this就会指向谁。真正调用user.clickHandler的对象是button对象—user.clickHandler会在button对象的单击方法里执行。

注意,尽管我们使用user.clickHandler来调用clickHander方法(我们也只能这么做,因为clickHandler是定义在user对象上的),clickHandler方法本身会被现在被this所指向的上下文对象所调用。所以this现在指向的是$(“button”)对象。

当上下文改变时—当我们在其它对象而非原对象上执行某个方法的时候,显然this关键字不再指向定义了this关键字的原对象。

解决方案:

由于我们真的很想让this.data指向user对象的data属性,我们可以使用Bind/ Apply/ Call等方法来强制改变this所指向的对象。本系列的其它篇目将专门对Bind/ Apply/ Call进行讲解,文中介绍了如何在不同的情况强制改变this的值的方法。与其在本文大篇幅讨论,我强烈建议大家直接去读另外的篇目(译者注:晚些时候放出这里所说的“其它篇目”)。

为了解决前面代码中的问题,我们可以使用bind方法。

针对下面这行代码:

$ ("button").click (user.clickHandler);
我们可以用bind方法把clickHandler绑定的user对象上:

$("button").click (user.clickHandler.bind (user)); // P. Mickelson 43
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没有太大问题,因为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,所以在非严格模式下,this指向了全局的window对象

解决方案:

在进入forEach方法之前,额外使用一个变量来引用this。

var user = {
 tournament: "The Masters",
 data: [{
  name: "T. Woods",
  age: 37
 },
 {
  name: "P. Mickelson",
  age: 43
 }],

 clickHandler: function(event) {
  //为了捕获this指向user对象时的值,我们把它赋值给另外一个变量theUserObj,后面我们可以使用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

正如下面的代码,很多JS开发人员喜欢使用变量that来设置this的值。但我个人不太喜欢用that这个名字,我喜欢使用让人一眼就能看懂this到底指向谁的那种名字,所以上面的代码中我使用了theUserObj = this。

// 这句代码对大多数JS开发人员来说再常见不过了
var that = this;

3.方法被赋值给某个变量

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; // 随机生成1或0
    //这句话会从数组data里随机显示人名和岁数
    console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
  }

}

// 把user.showData方法赋值给变量 showUserData
var showUserData = user.showData;

//执行showUserData方法,结果将 来自全局的data数组而非user对象的data属性
showUserData(); // Samantha 12 (来自全局变量data)
//解决方案:通过使用bind方法来显式设置this的值
//把showData方法绑定到user对象上
var showUserData = user.showData.bind(user);

//现在结果将来自user对象,因为this关键字已经被强制绑定到user对象上了
showUserData(); // P. Mickelson 43

4.借用方法带来的问题

JS开发中,借用方法(borrowing methods)很常见。

我们来看下面的代码:

//下面代码中有两个对象。其中一个定义了avg方法,另一个不包含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调用该方法,this将会指向appController(但事实上这并不是我们期望的结果,因为我们只想借用方法的实现逻辑而非具体的数据来源)。

解决方案:

为了保证gameController只借用appController的avg方法的逻辑,我们使用apply方法:

// 我们要使用apply方法,注意这里传入appController.avg方法的第二个参数
appController.avg.apply(gameController, gameController.scores);

//尽管avg方法是借来的,但是现在avgScore属性已经被成功地应用到gameController上了。
console.log(gameController.avgScore); // 46.4
//appController.avgScore仍然是null,只有gameController的avgScore被更新了
console.log(appController.avgScore); // null

gameController只借用了appController的avg方法,这时this将指向gameController,因为我们把gameController作为apply方法的第一个参数进行传递。apply方法的第一个参数将会显式设置this的取值。

结语

希望您在文中有所收获。现在你可以使用文中介绍的绝招(bind方法,apply方法,call方法,以及把this赋值给 一个变量)来对付跟this相关的任何问题。

正如已经了解到的,this在上下文改变、被作为回调函数使用、被不同的对象调用、或者方法被借用的情况下,this将一直指向调用当前方法的对象。

Javascript 相关文章推荐
Javascript倒计时页面跳转实例小结
Sep 11 Javascript
jquery解析xml字符串简单示例
Apr 11 Javascript
thinkphp 表名 大小写 窍门
Feb 01 Javascript
自己编写的支持Ajax验证的JS表单验证插件
May 15 Javascript
将html页面保存成图片,图片写入pdf的实现方法(推荐)
Sep 17 Javascript
浅谈js的异步执行
Oct 18 Javascript
微信小程序 Buffer缓冲区的详解
Jul 06 Javascript
vue+swiper实现组件化开发的实例代码
Oct 26 Javascript
vue 项目中使用Loading组件的示例代码
Aug 31 Javascript
JS滚轮控制图片缩放大小和拖动的实例代码
Nov 20 Javascript
ionic4+angular7+cordova上传图片功能的实例代码
Jun 19 Javascript
vue-axios同时请求多个接口 等所有接口全部加载完成再处理操作
Nov 09 Javascript
javascript正则表达式总结
Feb 29 #Javascript
javascript计时器编写过程与实现方法
Feb 29 #Javascript
javascript插件开发的一些感想和心得
Feb 28 #Javascript
详解Javascript中的Object对象
Feb 28 #Javascript
Jquery跨浏览器文本复制插件Zero Clipboard的使用方法
Feb 28 #Javascript
JavaScript实现iframe自动高度调整和不同主域名跨域
Feb 27 #Javascript
Javascript技术栈中的四种依赖注入小结
Feb 27 #Javascript
You might like
php 文件上传系统手记
2009/10/26 PHP
PHP判断图片格式的七种方法小结
2013/06/03 PHP
php 使用curl模拟登录人人(校内)网的简单实例
2016/06/06 PHP
一个刚完成的layout(拖动流畅,不受iframe影响)
2007/08/17 Javascript
关于js中window.location.href,location.href,parent.location.href,top.location.href的用法与区别
2010/10/18 Javascript
JS修改css样式style浅谈
2013/05/06 Javascript
超简单JS二级、多级联动的简单实例
2014/02/18 Javascript
用javascript将数据导入Excel示例代码
2014/09/10 Javascript
使用Javascript简单实现图片无缝滚动
2014/12/05 Javascript
整理AngularJS框架使用过程当中的一些性能优化要点
2016/03/05 Javascript
jquery popupDialog 使用 加载jsp页面的方法
2016/10/25 Javascript
NodeJS、NPM安装配置步骤(windows版本) 以及环境变量详解
2017/05/13 NodeJs
浅谈Vuex的状态管理(全家桶)
2017/11/04 Javascript
jquery获取select选中值的文本,并赋值给另一个输入框的方法
2018/08/21 jQuery
微信小程序mpvue点击按钮获取button值的方法
2019/05/29 Javascript
jquery UI实现autocomplete在获取焦点时得到显示列表功能示例
2019/06/04 jQuery
[01:11:21]DOTA2-DPC中国联赛 正赛 Phoenix vs CDEC BO3 第三场 3月7日
2021/03/11 DOTA
python中精确输出JSON浮点数的方法
2014/04/18 Python
Python采用raw_input读取输入值的方法
2014/08/18 Python
一张图带我们入门Python基础教程
2017/02/05 Python
浅谈python import引入不同路径下的模块
2017/07/11 Python
python中子类调用父类函数的方法示例
2017/08/18 Python
Python中装饰器学习总结
2018/02/10 Python
python删除不需要的python文件方法
2018/04/24 Python
pygame游戏之旅 添加碰撞效果的方法
2018/11/20 Python
Python PyCharm如何进行断点调试
2019/07/05 Python
Python turtle库的画笔控制说明
2020/06/28 Python
公司JAVA开发面试题
2015/04/02 面试题
计算机系毕业生推荐信
2013/11/06 职场文书
销售主管岗位职责
2014/02/08 职场文书
军训自我鉴定怎么写
2014/02/13 职场文书
校长师德师风自我剖析材料
2014/09/29 职场文书
工作违纪的检讨书范文
2019/07/09 职场文书
关于CentOS 8 搭建MongoDB4.4分片集群的问题
2021/10/24 MongoDB
Python开发五子棋小游戏
2022/04/28 Python
Docker容器harbor私有仓库部署和管理
2022/08/05 Servers