作者选择开放互联网 / 自由言论基金接受捐赠作为写作捐赠计划的一部分。

这个关键词在 JavaScript 中是一个非常重要的概念,对于新手开发者和那些有其他编程语言经验的人来说,这个关键词也是一个特别容易混淆的概念。 在 JavaScript 中,这是对一个对象的引用。 引用的对象可以根据它是全局的、对象的还是构造函数的而隐式地变化,也可以根据使用 Function prototype 方法 bind、 call 和 apply 而显式地变化。

虽然这是一个有点复杂的主题,但是它也是一个在您开始编写第一个 JavaScript 程序时就会出现的主题。 无论您是尝试访问文档对象模型(DOM)中的元素或事件,构建以面向对象程序设计 / 文档风格编写的类,还是使用常规对象的属性和方法,都会遇到这种情况。

在本文中,您将学习基于上下文隐式地指代什么,并学习如何使用绑定、调用和应用方法来显式地确定此值。

隐含的上下文

在以下四种主要情况下,可以隐含地推断出这一点的价值:

  • 全球环境
  • 作为对象中的一种方法
  • 作为函数或类的构造函数
  • 作为 DOM 事件处理程序

全球

在全局上下文中,这指的是全局对象。 当你在浏览器中工作时,全局上下文是一个可能的窗口。 当你在 Node.js 上工作时,全球环境是全球性的。

注意: 如果您还不熟悉 JavaScript 中作用域的概念,请查看 JavaScript 中的理解变量、作用域和吊装。

对于示例,您将在浏览器的 Developer Tools 控制台中练习代码。 如果您不熟悉在浏览器中运行 JavaScript 代码,请阅读如何使用 JavaScript 开发者控制台。

如果您在没有任何其他代码的情况下记录这个值,您将看到这个值指的是什么对象。

console.log(this)
Output
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

你可以看到这是一个窗口,它是一个浏览器的全局对象。

在 JavaScript 中理解变量、范围和提升时,您了解到函数对于变量有自己的上下文。 您可能会认为这在函数内部遵循同样的规则,但实际并非如此。 顶级函数仍将保留全局对象的这个引用。

你可以编写一个顶级函数,或者一个与任何对象都没有关联的函数,像这样:

function printThis() {
  console.log(this)
}

printThis()
Output
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

即使在函数中,这仍然指向窗口或全局对象。

但是,当使用 strict 模式时,这个函数在全局上下文中的上下文将是未定义的。

'use strict'

function printThis() {
  console.log(this)
}

printThis()
Output
undefined

通常,使用严格模式来降低出现意外范围的可能性会更加安全。 很少有人想用这个来引用窗口对象。

有关 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)
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 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 显式绑定的许多不同的值。 您还了解了如何使用箭头函数中缺少这种绑定来引用不同的上下文。 有了这些知识,您应该能够确定它在程序中的价值。