this
关键词在JavaScript
中是个很重要的概念,也是一个对初学者和学习其他语言的人来说晦涩难懂。在JavaScript
中,this
是一个对象的引用。this
指向的对象可以是基于全局的,在对象上的,或者在构造函数中隐式更改的,当然也可以根据Function
原型方法的bind
,call
和apply
使用显示更改的。
尽管this
是个复杂的话题,但是也是你开始编写第一个JavaScript
程序后出现的话题。无论你尝试访问the Document Object Model (DOM)中的元素或事件,还是以面向对象的编程风格来构建用于编写的类,还是使用常规对象的属性和方法,都见遇到this
。
在这篇文章中,你将学习到基于上下文隐式表示的含义,并将学习如何使用bind
,call
和apply
方法来显示确定this
的值。
隐式上下文
在四个主要上下文中,我们可以隐式地推断出this
的值:
- 全局上下文
- 作为对象内的方法
- 作为函数或类的构造函数
- 作为DOM事件处理程序
全局
在全局上下文中,this
指向全局对象。当你使用浏览器,全局上下文将是window
。当你使用Node.js,全局上下文就是global
。
备注:如果你对JavaScript中得作用域概念不熟,你可以去[Understanding Variables, Scope, and Hoisting in JavaScript温习一下。
针对例子,你可以在浏览器的开发者工具栏中验证。如果你不是很熟悉在浏览器中运行JavaScript
代码,可以去阅读下How to Use the JavaScript Developer Console 文章。
如果你只是简单打印this
,你将看到this
指向的对象是什么。
console.log(this) 复制代码
Output Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …} 复制代码
你可以看到,this
就是window
,也就是浏览器的全局对象。
在Understanding Variables, Scope, and Hoisting in JavaScript中,你学习到函数中的变量有自己的上下文。你可能会认为,在函数内部this
会遵循相同的规则,但是并没有。顶层的函数中,this
仍然指向全局对象。
你可以写一个顶层的函数,或者是一个没有关联任何对象的函数,比如下面这个:
function printThis() { console.log(this) } printThis() 复制代码
Output Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …} 复制代码
即使在一个函数中,this
仍然指向了window
,或全局对象。
然而,当使用严格模式,全局上下文中,函数内this
的上下文指向undefined
。
'use strict' function printThis() { console.log(this) } printThis() 复制代码
Output undefined 复制代码
总的来说,使用严格模式更加安全,能减少this
产生的非预期作用域的可能性。很少有人想直接将this
指向window
对象。
有关严格模式以及对错误和安全性所做更改的详细信息,请阅读MDN上Strict mode的文档
对象方法
一个方法是对象上的函数,或对象可以执行的一个任务。方法使用this
来引用对象的属性。
const america = { name: 'The United States of America', yearFounded: 1776, describe() { console.log(`${this.name} was founded in ${this.yearFounded}.`) }, } america.describe() 复制代码
Output "The United States of America was founded in 1776." 复制代码
在这个例子中,this
等同于america
。
在嵌套对象中,this
指向方法当前对象的作用域。在下面这个例子,details
对象中的this.symbol
指向details.symbol
。
const america = { name: 'The United States of America', yearFounded: 1776, details: { symbol: 'eagle', currency: 'USD', printDetails() { console.log(`The symbol is the ${this.symbol} and the currency is ${this.currency}.`) }, }, } america.details.printDetails() 复制代码
Output "The symbol is the eagle and the currency is USD." 复制代码
另一种思考的方式是,在调用方法时,this
指向.
左侧的对象。
函数构造器
当你使用new关键字,会创建一个构造函数或类的实例。在ECMAScript 2015
更新为JavaScript
引入类语法之前,构造函数是初始化用户定义对象的标准方法。在Understanding Classes in JavaScript中,你将学到怎么去创建一个函数构造器和等效的类构造函数。
function Country(name, yearFounded) { this.name = name this.yearFounded = yearFounded this.describe = function() { console.log(`${this.name} was founded in ${this.yearFounded}.`) } } const america = new Country('The United States of America', 1776) america.describe() 复制代码
Output "The United States of America was founded in 1776." 复制代码
在这个上下文中,现在this
绑定到Country
的实例,该实例包含在America
常量中。
类构造器
类上的构造函数的作用与函数上的构造函数的作用相同。在Understanding Classes in JavaScript中,你可以了解到更多的关于构造函数和ES6
类的相似和不同的地方。
class Country { constructor(name, yearFounded) { this.name = name this.yearFounded = yearFounded } describe() { console.log(`${this.name} was founded in ${this.yearFounded}.`) } } const america = new Country('The United States of America', 1776) america.describe() 复制代码
describe
方法中的this
指向Country
的实例,即america
。
Output "The United States of America was founded in 1776." 复制代码
DOM事件处理程序
在浏览器中,事件处理程序有一个特殊的this
上下文。在被称为addEventListener
调用的事件处理程序中,this
将指向event.currentTarget
。开发人员通常会根据需要简单地使用event.target
或event.currentTarget
来访问DOM
中的元素,但是由于this
引用在此上下文中发生了变化,因此了解这一点很重要。
在下面的例子,我们将创建一个按钮,为其添加文字,然后将它追加到DOM中。当我们使用事件处理程序打印其this
的值,它将打印目标内容。
const button = document.createElement('button') button.textContent = 'Click me' document.body.append(button) button.addEventListener('click', function(event) { console.log(this) }) 复制代码
Output <button>Click me</button> 复制代码
如果你复制上面的代码到你的浏览器运行,你将看到一个有Click me
按钮的页面。如果你点击这个按钮,你会看到<button>Click me</button>
出现在控制台上,因为点击按钮打印的元素就是按钮本身。因此,正如你所看到的,this
指向的目标元素,就是我们向其中添加了事件监听器的元素。
显式上下文
在所有的先前的例子中,this
的值取决于其上下文 -- 在全局的,在对象中,在构造函数或类中,还是在DOM
事件处理程序上。然而,使用call, apply
或 bind
,你可以显示地决定this
应该指向哪。
决定什么时候使用call, apply
或 bind
是一件很困难的事情,因为它将决定你程序的上下文。当你想使用事件来获取嵌套类中的属性时,bind
可能有用。比如,你写一个简单的游戏,你可能需要在一个类中分离用户接口和I/O
,然后游戏的逻辑和状态是在另一个类中。由于游戏逻辑需要用户输入,比如按键或点击事件,你可能想要bind
事件去获取游戏逻辑类中的this
的值。
最重要的部分是,要知道怎么决定this
对象指向了哪,这样你就可以像之前章节学的隐式操作那样操作,或者通过下面的三种方法显示操作。
Call 和 Apply
call
和apply
非常相似--它们都调用一个带有特定this
上下文和可选参数的函数。call
和apply
的唯一区别就是,call
需要一个个的传可选参数,而apply
只需要传一个数组的可选参数。
在下面这个例子中,我们将创建一个对象,创建一个this
引用的函数,但是this
没有明确上下文(其实this默认指向了window)。
const book = { title: 'Brave New World', author: 'Aldous Huxley', } function summary() { console.log(`${this.title} was written by ${this.author}.`) } summary() 复制代码
Output "undefined was written by undefined" 复制代码
因为summary
和book
没有关联,调用summary
本身将只会打印出undefined
,其在全局对象上查找这些属性。
备注: 在严格模式中尝试
this
会返回Uncaught TypeError: Cannot read property 'title' of undefined
的错误结果,因为this
它自身将会是undefined
然而,你可以在函数中使用call
和apply
调用book
的上下文this
。
summary.call(book) // or: summary.apply(book) 复制代码
Output "Brave New World was written by Aldous Huxley." 复制代码
现在,当上面的方法运用了,book
和summary
之间有了关联。我们来确认下,现在this
到底是什么。
function printThis() { console.log(this) } printThis.call(book) // or: whatIsThis.apply(book) 复制代码
Output {title: "Brave New World", author: "Aldous Huxley"} 复制代码
在这个案例中,this
实际上变成的所传参数的对象。
这就是说call
和apply
一样,但是它们又有点小区别。
除了将第一个参数作为this
上下文传递之外,你也可以传递其他参数。
function longerSummary(genre, year) { console.log( `${this.title} was written by ${this.author}. It is a ${genre} novel written in ${year}.` ) } 复制代码
使用call
时,你使用的每个额外的值都会被作为附加参数进行传递。
longerSummary.call(book, 'dystopian', 1932) 复制代码
Output "Brave New World was written by Aldous Huxley. It is a dystopian novel written in 1932." 复制代码
如果你尝试使用apply
去发送相同的参数,就会发生下面的事情:
longerSummary.apply(book, 'dystopian', 1932) 复制代码
Output Uncaught TypeError: CreateListFromArrayLike called on non-object at <anonymous>:1:15 复制代码
针对apply
,作为替代,你需要将参数放在一个数组中传递。
longerSummary.apply(book, ['dystopian', 1932]) 复制代码
Output "Brave New World was written by Aldous Huxley. It is a dystopian novel written in 1932." 复制代码
通过单个参数传递和形成一个数组参数传递,两个之间的差别是微妙的,但是值得你留意。使用apply
更加简单和方便,因为如果一些参数的细节改变了,它不需要改变函数调用。
Bind
call
和apply
都是一次性使用的方法 -- 如果你调用带有this
上下文的方法,它将含有此上下文,但是原始的函数依旧没改变。
有时候,你可能需要重复地使用方法来调用另一个对象的上下文,所以,在这种场景下你应该使用bind
方法来创建一个显示调用this
的全新函数。
const braveNewWorldSummary = summary.bind(book) braveNewWorldSummary() 复制代码
Output "Brave New World was written by Aldous Huxley" 复制代码
在这个例子中,每次你调用braveNewWorldSummary
,它都会返回绑定它的原始this
值。尝试绑定一个新的this
上下文将会失败。因此,你始终可以信任绑定的函数来返回你期待的this
值。
const braveNewWorldSummary = summary.bind(book) braveNewWorldSummary() // Brave New World was written by Aldous Huxley. const book2 = { title: '1984', author: 'George Orwell', } braveNewWorldSummary.bind(book2) braveNewWorldSummary() // Brave New World was written by Aldous Huxley. 复制代码
虽然这个例子中braveNewWorldSummary
尝试再次绑定bind
,它依旧保持着第一次绑定就保留的this
上下文。
箭头函数
Arrow functions没有自己的this
绑定。相反,它们上升到下一个执行环境。
const whoAmI = { name: 'Leslie Knope', regularFunction: function() { console.log(this.name) }, arrowFunction: () => { console.log(this.name) }, } whoAmI.regularFunction() // "Leslie Knope" whoAmI.arrowFunction() // undefined 复制代码
在你想将this
执行外部上下文的情况下,箭头函数会很有用。比如,在类中有一个事件监听器,你可能想将this
指向此类中的一些值。
在下面这个例子中,像之前一样,你将创建一个按钮并将其追加到DOM
中,但是,类中将会有一个事件监听器,当按钮被点击时候会改变其文本值。
const button = document.createElement('button') button.textContent = 'Click me' document.body.append(button) class Display { constructor() { this.buttonText = 'New text' button.addEventListener('click', event => { event.target.textContent = this.buttonText }) } } new Display() 复制代码
如果你点击按钮,其文本会变成buttonText
的值。如果在这里,你并没有使用箭头函数,this
将等同于event.currentTarget
,如没有显示绑定this
,你将不能获取类中的值。这种策略通常使用在像React
这样框架的类方法上。
总结
在这篇文章中,你学到了关于JavaScript
的this
,和基于隐式运行时绑定的可能具有的不同值,以及通过bind
,call
和apply
的显示绑定。你还了解到了如何使用箭头函数缺少this
绑定来指向不同的上下文。有了这些知识,你应该能够在你的程序中明确this
的价值了。