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)
代码语言:javascript复制Output
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
你可以看到,this
就是window
,也就是浏览器的全局对象。
在Understanding Variables, Scope, and Hoisting in JavaScript中,你学习到函数中的变量有自己的上下文。你可能会认为,在函数内部this
会遵循相同的规则,但是并没有。顶层的函数中,this
仍然指向全局对象。
你可以写一个顶层的函数,或者是一个没有关联任何对象的函数,比如下面这个:
代码语言:javascript复制function printThis() {
console.log(this)
}
printThis()
代码语言:javascript复制Output
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
即使在一个函数中,this
仍然指向了window
,或全局对象。
然而,当使用严格模式,全局上下文中,函数内this
的上下文指向undefined
。
'use strict'
function printThis() {
console.log(this)
}
printThis()
代码语言:javascript复制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()
代码语言:javascript复制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()
代码语言:javascript复制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()
代码语言:javascript复制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)
})
代码语言:javascript复制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()
代码语言:javascript复制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)
代码语言:javascript复制Output
"Brave New World was written by Aldous Huxley."
现在,当上面的方法运用了,book
和summary
之间有了关联。我们来确认下,现在this
到底是什么。
function printThis() {
console.log(this)
}
printThis.call(book)
// or:
whatIsThis.apply(book)
代码语言:javascript复制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)
代码语言:javascript复制Output
"Brave New World was written by Aldous Huxley. It is a dystopian novel written in 1932."
如果你尝试使用apply
去发送相同的参数,就会发生下面的事情:
longerSummary.apply(book, 'dystopian', 1932)
代码语言:javascript复制Output
Uncaught TypeError: CreateListFromArrayLike called on non-object at <anonymous>:1:15
针对apply
,作为替代,你需要将参数放在一个数组中传递。
longerSummary.apply(book, ['dystopian', 1932])
代码语言:javascript复制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()
代码语言:javascript复制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`中,但是,类中将会有一个事件监听器,当按钮被点击时候会改变其文本值。
```javascript
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
的价值了。