理解 JavaScript 中的 This、Bind、Call 和 Apply
作者选择开放互联网/言论自由基金作为Write for DOnations计划的一部分接受捐赠。
关键字this
是 JavaScript 中一个非常重要的概念,但对于新手和有其他编程语言经验的人来说,它却是一个特别容易混淆的概念。在 JavaScript 中,this
是对一个对象的引用。 所引用的对象this
可能会根据其是全局变量、对象本身还是构造函数而隐式变化,也可能会根据原型Function
方法bind
、call
和 的使用而显式变化apply
。
虽然this
这是一个有点复杂的话题,但它也是你开始编写第一个 JavaScript 程序时就会遇到的问题。无论你是尝试访问文档对象模型 (DOM)中的元素或事件,构建面向对象编程风格的类,还是使用常规对象的属性和方法,你都会遇到this
。
在本文中,您将了解this
根据上下文隐式地引用什么,以及如何使用bind
、call
和apply
方法显式地确定的值this
。
隐式上下文
有四种主要上下文this
可以隐式推断出的值:
- 全球背景
- 作为对象内的方法
- 作为函数或类的构造函数
- 作为 DOM 事件处理程序
全球的
在全局上下文中,this
指的是全局对象。在浏览器中工作时,全局上下文是window
。在 Node.js 中工作时,全局上下文是global
。
注意:如果您还不熟悉 JavaScript 中的作用域概念,请查看理解 JavaScript 中的变量、作用域和提升。
在本例中,您将在浏览器的开发者工具控制台中练习代码。如果您不熟悉如何在浏览器中运行 JavaScript 代码,请阅读如何使用 JavaScript 开发者控制台。
this
如果您记录不带任何其他代码的值,您将看到对象this
所引用的内容。
console.log(this)
Output
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
您可以看到,this
它window
是浏览器的全局对象。
在理解 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
。很少有人会想用window
来引用对象this
。
有关严格模式的更多信息以及它在错误和安全性方面所做的更改,请阅读MDN 上的严格模式文档。
对象方法
方法是对象上的函数,或者对象可以执行的任务。方法用于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
指的是方法的当前对象范围。在下面的示例中,对象this.symbol
内details
指的是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
关键字时,它会创建一个构造函数或类的实例。class
在 ECMAScript 2015 JavaScript 更新中引入该语法之前,函数构造函数是初始化用户定义对象的标准方法。在理解 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
常量中。
类构造函数
类的构造函数与函数的构造函数作用相同。请参阅《理解 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()
this
方法中的describe
指的是 的实例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
非常相似——它们都使用指定的上下文和可选参数来调用函数。和this
之间的唯一区别在于要求参数逐个传入,而则以数组形式接收参数。call
apply
call
apply
this
在这个例子中,我们将创建一个对象,并创建一个引用但没有上下文的函数this
。
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
,因为它正在寻找全局对象上的那些属性。
注意:在严格模式下尝试此操作将导致Uncaught TypeError: Cannot read property 'title' of undefined
,因为this
它本身就是undefined
。
但是,您可以使用call
和apply
来调用函数this
的上下文。book
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
,因为即使某些参数细节发生变化,也无需更改函数调用。
绑定
和call
都是apply
一次性使用的方法——如果你使用上下文调用该方法,this
它将拥有它,但原始功能将保持不变。
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
尽管此示例尝试再次绑定,但它保留了this
第一次绑定时的原始上下文。
箭头函数
箭头函数没有自己的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
,并且您将无法在未显式绑定的情况下使用它来访问类中的值。这种策略通常用于 React 等框架中的类方法。
结论
在本文中,您了解了this
JavaScript 中的 ,以及基于隐式运行时绑定和通过bind
、call
和 的显式绑定,它可能具有的众多不同值。apply
您还了解了如何利用箭头函数中缺少的this
绑定来引用不同的上下文。掌握这些知识后,您应该能够确定this
程序中 的值。
本作品采用Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议进行许可
文章来源:https://dev.to/digitalocean/understanding-this-bind-call-and-apply-in-javascript-dla