理解 JS 中的 This, Bind, Call, 和 Apply

原文在 https://www.digitalocean.com/community/conceptual_articles/understanding-this-bind-call-and-apply-in-javascript
作者是Tania Rascia


作者选择了“开放互联网/言论自由基金会””作为“Write for DOnations””计划的一部分来接受捐赠。

this关键字是在JavaScript中一个非常重要的概念,也是一个特别令人迷惑的这两个新的开发者和那些谁拥有在其他编程语言的经验。在JavaScript中,this是对对象的引用。该对象this是指可以改变,含蓄地基于它是否是全球性的,在对象上,或者在一个构造函数,也可以明确地变化根据的使用Function原型方法bindcallapply

尽管这this是一个复杂的话题,但它也是在您开始编写第一个JavaScript程序后立即出现的话题。无论您是尝试访问文档对象模型(DOM)中的元素还是事件,构建用于以面向对象的编程风格编写的类,还是使用常规对象的属性和方法,都将遇到this

在本文中,您将了解什么this是指隐含根据上下文,您将学习如何使用bindcallapply方法,明确确定的值this

内隐语境

在以下四个主要上下文中,this可以隐式推断出的值:

  • 全局背景
  • 作为对象内的方法
  • 作为函数或类的构造函数
  • 作为DOM事件处理程序

全局

在全局上下文中, this指的是全局对象。在浏览器中工作时,全局上下文为window。当您使用Node.js时,全局上下文为global

注意:如果您还不熟悉JavaScript中范围的概念,请参阅了解 JavaScript中的变量,范围和提升

对于示例,您将在浏览器的Developer Tools控制台中练习代码。如果您不熟悉在浏览器中运行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.symboldetails对象内是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()

thisdescribe方法指的实例Country,这是america

Output
"The United States of America was founded in 1776."

DOM事件处理程序

在浏览器中,this事件处理程序具有特殊的上下文。在由调用的事件处理程序中addEventListenerthis将引用event.currentTarget。开发人员通常会简单地根据需要使用event.targetevent.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>

将其粘贴到浏览器中后,您将在页面上看到一个“点击我”按钮。如果单击该按钮,您将看到<button>Click me</button>出现在您的控制台中,因为单击该按钮会记录元素,该元素就是按钮本身。因此,如您所见,this指向目标元素,这是我们向其添加事件侦听器的元素。

显式上下文

在前面的所有示例中,的值this都是由其上下文确定的-无论它是全局的,在对象中,在构造的函数或类中还是在DOM事件处理程序上。但是,使用callapplybind可以显式确定this应引用的内容。

很难准确定义何时使用callapplybind,因为这将取决于程序的上下文。bind当您想使用事件来访问另一个类中一个类的属性时,它可能特别有用。例如,如果要编写一个简单的游戏,则可以将用户界面和I / O分成一个类,将游戏逻辑和状态分成另一个类。由于游戏逻辑需要访问输入,例如按键和单击,因此您希望bind事件能够访问this游戏逻辑类的值。

重要的部分是要知道如何确定this引用的对象,您可以使用上一部分中的内容隐式地进行操作,或者使用接下来将要学习的三种方法来显式地进行操作。

call apply

callapply它们非常相似-它们调用具有指定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

但是,您可以使用callapply调用函数上的this上下文book

summary.call(book)
// or:
summary.apply(book)
Output
"Brave New World was written by Aldous Huxley."

现在booksummary在应用这些方法之间以及何时应用这些方法之间存在联系。让我们确切地确认是什么this

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

printThis.call(book)
// or:
whatIsThis.apply(book)
Output
{title: "Brave New World", author: "Aldous Huxley"}

在这种情况下,this实际上成为了作为参数传递的对象。

这就是为何callapply是相同的,但有一个小的差异。除了能够将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

这两个callapply是一次性使用的方法,如果调用该方法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,和许多不同的值,可能已经基于隐式运行时绑定,并明确通过结合bindcallapply。您还了解了如何this使用箭头功能中缺少绑定的方式来引用不同的上下文。有了这些知识,您应该能够确定程序中的价值this


搬运一篇文章,机翻。
没有公众号需要你关注。

相关推荐