作者选择开放互联网 / 自由言论基金接受捐赠作为写作捐赠计划的一部分。
这个关键词在 JavaScript 中是一个非常重要的概念,对于新手开发者和那些有其他编程语言经验的人来说,这个关键词也是一个特别容易混淆的概念。 在 JavaScript 中,这是对一个对象的引用。 引用的对象可以根据它是全局的、对象的还是构造函数的而隐式地变化,也可以根据使用 Function prototype 方法 bind、 call 和 apply 而显式地变化。
虽然这是一个有点复杂的主题,但是它也是一个在您开始编写第一个 JavaScript 程序时就会出现的主题。 无论您是尝试访问文档对象模型(DOM)中的元素或事件,构建以面向对象程序设计 / 文档风格编写的类,还是使用常规对象的属性和方法,都会遇到这种情况。
在本文中,您将学习基于上下文隐式地指代什么,并学习如何使用绑定、调用和应用方法来显式地确定此值。
隐含的上下文
在以下四种主要情况下,可以隐含地推断出这一点的价值:
- 全球环境
- 作为对象中的一种方法
- 作为函数或类的构造函数
- 作为 DOM 事件处理程序
全球
在全局上下文中,这指的是全局对象。 当你在浏览器中工作时,全局上下文是一个可能的窗口。 当你在 Node.js 上工作时,全球环境是全球性的。
注意: 如果您还不熟悉 JavaScript 中作用域的概念,请查看 JavaScript 中的理解变量、作用域和吊装。
对于示例,您将在浏览器的 Developer Tools 控制台中练习代码。 如果您不熟悉在浏览器中运行 JavaScript 代码,请阅读如何使用 JavaScript 开发者控制台。
如果您在没有任何其他代码的情况下记录这个值,您将看到这个值指的是什么对象。
console.log(this)
OutputWindow {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
你可以看到这是一个窗口,它是一个浏览器的全局对象。
在 JavaScript 中理解变量、范围和提升时,您了解到函数对于变量有自己的上下文。 您可能会认为这在函数内部遵循同样的规则,但实际并非如此。 顶级函数仍将保留全局对象的这个引用。
你可以编写一个顶级函数,或者一个与任何对象都没有关联的函数,像这样:
function printThis() {
console.log(this)
}
printThis()
OutputWindow {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
即使在函数中,这仍然指向窗口或全局对象。
但是,当使用 strict 模式时,这个函数在全局上下文中的上下文将是未定义的。
'use strict'
function printThis() {
console.log(this)
}
printThis()
Outputundefined
通常,使用严格模式来降低出现意外范围的可能性会更加安全。 很少有人想用这个来引用窗口对象。
有关 Strict 模式及其在错误和安全性方面所做的更改的详细信息,请参阅 MDN 上的 Strict 模式文档。
对象方法
方法是对象上的函数,或者是对象可以执行的任务。 方法使用它来引用对象的属性。
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."
在这个例子中,这和美国是一样的。
在嵌套对象中,这指的是方法的当前对象范围。 在下面的示例中,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."
另一种思考方式是,当调用方法时,它指向点左侧的对象。
函数构造器
当您使用 new 关键字时,它将创建构造函数或类的实例。 在 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."
在这个上下文中,这个例子现在被绑定到国家,它包含在美国常数中。
类构造器
类上的构造函数与函数上的构造函数的作用相同。 在 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()
这里的描述方法指的是国家的例子,也就是美国。
Output"The United States of America was founded in 1776."
Dom 事件处理程序
在浏览器中,事件处理程序有一个特殊的 this 上下文。 在 addEventListener 调用的事件处理程序中,这将引用 event.currentTarget。 通常情况下,开发人员会根据需要简单地使用 event.target 或 event.currentTarget 来访问 DOM 中的元素,但是由于这个引用在这个上下文中发生了变化,了解这一点很重要。
在下面的示例中,我们将创建一个按钮,向其添加文本,并将其附加到 DOM。 当我们在事件处理程序中记录这个值时,它将打印目标。
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 / 按钮出现在您的控制台中,因为单击该按钮将记录元素,即按钮本身。 因此,正如您可以看到的,这引用了目标元素,该元素是我们添加的事件侦听器元素。
明确的上下文
在前面的所有示例中,它的值都是由其上下文决定的ーー无论它是全局的、对象中的、构造的函数或类中的,还是在 DOM 事件处理程序中的。 但是,使用调用、应用或绑定,您可以显式地确定这应该指的是什么。
很难确切地定义何时使用调用、应用或绑定,因为它取决于程序的上下文。 当您希望使用事件访问另一个类中一个类的属性时,bind 可能特别有用。 例如,如果要编写一个简单的游戏,可以将用户界面和 i / o 分离到一个类中,将游戏逻辑和状态分离到另一个类中。 因为游戏逻辑需要访问输入,比如按键和点击,所以需要绑定事件来访问游戏逻辑类的这个值。
重要的部分是知道如何确定这个对象指向哪个对象,您可以用在前面章节中学到的内容来隐式地确定这个对象,或者用接下来学到的三种方法来显式地确定这个对象。
打电话申请
Call 和 apply 非常相似ーー它们调用具有指定此上下文的函数和可选参数。 调用和 apply 之间的唯一区别是,调用要求一个接一个地传递参数,而 apply 将参数作为数组传递。
在本例中,我们将创建一个对象,并创建一个引用此对象但没有此上下文的函数。
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 本身只会打印未定义的内容,因为它正在全局对象上寻找这些属性。
注意: 在严格模式下尝试这样做会导致 Uncaught TypeError: 不能读取未定义的属性‘ title’ ,因为它本身是未定义的。
但是,您可以使用 call 和 apply 在函数上调用 book 的这个上下文。
summary.call(book)
// or:
summary.apply(book)
Output"Brave New World was written by Aldous Huxley."
在应用这些方法时,书和摘要之间现在有了联系。 让我们确认一下这到底是什么。
function printThis() {
console.log(this)
}
printThis.call(book)
// or:
whatIsThis.apply(book)
Output{title: "Brave New World", author: "Aldous Huxley"}
在这种情况下,它实际上变成了作为参数传递的对象。
这就是为什么调用和应用是相同的,但是有一个小区别。 除了能够将这个上下文作为第一个参数传递之外,还可以传递其他参数。
function longerSummary(genre, year) {
console.log(
`${this.title} was written by ${this.author}. It is a ${genre} novel written in ${year}.`
)
}
随着调用,您想要传递的每个附加值都作为附加参数发送。
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)
OutputUncaught 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 context 调用该方法,它将拥有它,但原始函数将保持不变。
有时,您可能需要在另一个对象的 this 上下文中反复使用一个方法,在这种情况下,您可以使用 bind 方法创建一个全新的函数,并显式地绑定这个函数。
const braveNewWorldSummary = summary.bind(book)
braveNewWorldSummary()
Output"Brave New World was written by Aldous Huxley"
在本例中,每次调用 braveNewWorldSummary 时,它总是返回绑定到它的原始值。 尝试将一个新的 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,但它保留了第一次绑定时的原始上下文。
箭头函数
箭头函数没有自己的此绑定。 相反,他们上升到执行的下一个层次。
const whoAmI = {
name: 'Leslie Knope',
regularFunction: function() {
console.log(this.name)
},
arrowFunction: () => {
console.log(this.name)
},
}
whoAmI.regularFunction() // "Leslie Knope"
whoAmI.arrowFunction() // undefined
在您确实希望使用箭头函数引用外部上下文的情况下,使用箭头函数可能很有用。 例如,如果类中有一个事件侦听器,您可能希望它引用类中的某个值。
在这个示例中,您将像前面一样创建并向 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 的值。 如果在这里没有使用箭头函数,那么这将等于 event.currentTarget,如果没有显式地绑定它,就不能使用它来访问类中的值。 这种策略经常用于 React 这样的框架中的类方法。
总结
在本文中,您在 JavaScript 中了解了这一点,以及基于隐式运行时绑定和通过 bind、 call 和 apply 显式绑定的许多不同的值。 您还了解了如何使用箭头函数中缺少这种绑定来引用不同的上下文。 有了这些知识,您应该能够确定它在程序中的价值。