理解JavaScript中“this”关键字以及call、apply、bind

this关键字是JavaScript中最容易被误解的方面之一在这篇文章中,您将学习五个规则来确定this关键字引用的内容。隐式绑定,显式绑定,新绑定,窗口绑定和词法绑定。在覆盖这些方法,您还可以学习一些JavaScript的其他混淆部件以及类似.call.apply.bindnew关键字。

在深入this研究JavaScript中关键字的细节之前,重要的是退后一步,首先看一下this关键字存在的原因this关键字允许您重用具有不同上下文的函数。换句话说,“this”关键字允许您在调用函数或方法时决定哪个对象应该是焦点。在此之后我们谈论的一切都将建立在这个想法之上。我们希望能够在不同的上下文中或在不同的对象中重用函数或方法。

我们要看的第一件事是如何判断this关键字引用的内容。当你试图回答这个问题时,你需要问自己的第一个也是最重要的问题是“ 这个函数在哪里被调用?”。您可以通过查看调用使用关键字的函数的位置来判断关键字引用唯一方法this

为了用一个你已经熟悉的例子证明这一点,我们说我们有一个greet函数,它接受了一个名为一个警告的欢迎信息。

function greet (name) {
  alert(`Hello, my name is ${name}`)
}

如果我要问你到底greet会发出什么警报,你的回答是什么?只给出函数定义,就不可能知道。为了知道是什么name,你必须看看函数的调用greet

greet('Tyler')

通过弄清楚this关键字引用的内容,这是完全相同的想法您甚至可以this像对函数的正常参数一样考虑关键字 – 它将根据函数的调用方式进行更改。

现在您已经知道了确定this关键字引用内容的第一步是查看调用函数的位置,下一步是什么?为了帮助我们完成下一步,我们将制定5条规则或指南。

  1. 隐式绑定
  2. 显式绑定
  3. new绑定
  4. Lexical绑定
  5. window绑定

隐式绑定

请记住,此处的目标是能够使用this关键字查看函数定义并告知this引用的内容。这样做的第一个也是最常见的规则称为Implicit Binding我会说它会告诉你this关键字在80%的时间内引用了什么

假设我们有一个看起来像这样的对象

const user = {
  name: 'Tyler',
  age: 27,
  greet() {
    alert(`Hello, my name is ${this.name}`)
  }
}

现在,如果你要greetuser对象调用方法,那么你可以使用点表示法。

user.greet()

这将我们带到隐式绑定规则的主要关键点。为了弄清楚this关键字引用的内容,首先,在调用函数时查看点的左侧如果存在“点”,请查看该点的左侧以查找this关键字引用的对象

在上面的例子中,user是“点的左边”,这意味着this关键字引用了user对象。因此,就好像greet在JavaScript方法内部,JavaScript解释器更改thisuser

greet() {
  // alert(`Hello, my name is ${this.name}`)
  alert(`Hello, my name is ${user.name}`) // Tyler
}

让我们来看一个类似但稍微更高级的例子。现在,而不是仅仅有一个nameagegreet财产,让我们也给我们的用户对象一个mother也有一个属性namegreet属性。

const user = {
  name: 'Tyler',
  age: 27,
  greet() {
    alert(`Hello, my name is ${this.name}`)
  },
  mother: {
    name: 'Stacey',
    greet() {
      alert(`Hello, my name is ${this.name}`)
    }
  }
}

现在问题变成了,下面的每个调用会发出什么警报?

user.greet()
user.mother.greet()

每当我们试图找出this关键字引用的内容时,我们需要查看调用并查看“左边的点”是什么。在第一次调用中,user位于点的左侧,表示this将引用user在第二次调用中,mother位于点的左侧,表示this将引用mother

user.greet() // Tyler
user.mother.greet() // Stacey

如前所述,大约80%的时间会有一个“左边的点”的对象。这就是为什么在弄清楚this关键字引用的内容时应该采取的第一步是“向左看点”。但是,如果没有点怎么办?这将我们带入下一个规则 –

显式绑定

现在如果不是我们的greet函数是user对象的方法,它只是它自己的独立函数。

function greet () {
  alert(`Hello, my name is ${this.name}`)
}

const user = {
  name: 'Tyler',
  age: 27,
}

我们知道,为了告诉this关键字引用的内容,我们首先要查看函数的调用位置。现在这提出了一个问题,我们如何调用,greet但是使用this引用该user对象关键字来调用它我们不能user.greet()像以前那样做,因为user没有greet方法。在JavaScript中,每个函数都包含一个方法,允许您完成此操作并命名该方法call

“call”是每个函数的一个方法,它允许您调用函数,指定调用函数的上下文。

考虑到这一点,我们可以greet在user以下代码的上下文中调用

greet.call(user)

同样,call是每个函数的属性,并且传递给它的第一个参数将是调用函数的上下文(或焦点对象)。换句话说,传递给调用的第一个参数将是this该函数内部的关键字引用的内容。

这是规则#2(显式绑定)的基础,因为我们明确地(使用.call),指定this关键字引用的内容。

现在让我们greet稍微修改一下我们的功能。如果我们还想传递一些论点怎么办?与他们的名字一起说,我们也想提醒他们所知道的语言。像这样的东西

function greet (l1, l2, l3) {
  alert(
    `Hello, my name is ${this.name} and I know ${l1}, ${l2}, and ${l3}`
  )
}

现在,为了将参数传递给正在调用的函数.call,在指定第一个作为上下文的参数后,将它们逐个传递。

function greet (l1, l2, l3) {
  alert(
    `Hello, my name is ${this.name} and I know ${l1}, ${l2}, and ${l3}`
  )
}

const user = {
  name: 'Tyler',
  age: 27,
}

const languages = ['JavaScript', 'Ruby', 'Python']

greet.call(user, languages[0], languages[1], languages[2])

这有效,它显示了如何将参数传递给正在调用的函数.call。但是,正如您可能已经注意到的那样,必须从languages数组中逐个传递参数,这有点烦人。如果我们可以将整个数组作为第二个参数传入并且JavaScript会将它们传播给我们,那将是很好的。对我们来说这是个好消息,这正是这样.apply做的。.apply是完全相同的.call,但不是逐个传递参数,你可以传入一个数组,它会将数组中的每个元素作为函数的参数传播出去。

所以现在使用.apply,我们的代码可以改为这个(下面),其他一切都保持不变。

const languages = ['JavaScript', 'Ruby', 'Python']

// greet.call(user, languages[0], languages[1], languages[2])
greet.apply(user, languages)

到目前为止,在我们“显式绑定”的规则,我们已经了解.call,以及.apply它都允许你调用一个函数,指定什么this关键词是怎么回事该函数内被引用。这条规则的最后一部分是.bind.bind是完全相同.call但不是立即调用该函数,它将返回一个您可以在以后调用的新函数。因此,如果我们查看之前使用的代码.bind,它将看起来像这样

function greet (l1, l2, l3) {
  alert(
    `Hello, my name is ${this.name} and I know ${l1}, ${l2}, and ${l3}`
  )
}

const user = {
  name: 'Tyler',
  age: 27,
}

const languages = ['JavaScript', 'Ruby', 'Python']

const newFn = greet.bind(user, languages[0], languages[1], languages[2])
newFn() // alerts "Hello, my name is Tyler and I know JavaScript, Ruby, and Python"

new绑定

确定this关键字引用内容的第三条规则称为new绑定。如果您不熟悉newJavaScript中关键字,那么每当您使用new关键字调用函数时,JavaScript解释器都会为您创建一个全新的对象并进行调用this因此,自然地,如果调用函数new,则this关键字引用解释器创建的新对象。

function User (name, age) {
  /*
    Under the hood, JavaScript creates a new object called `this`
    which delegates to the User's prototype on failed lookups. If a
    function is called with the new keyword, then it's this new object
    that interpretor created that the this keyword is referencing.
  */

  this.name = name
  this.age = age
}

const me = new User('Tyler', 27)

Lexical 绑定

在这一点上,我们处于第四条规则,你可能会感到有些不知所措。这还算公平。thisJavaScript中关键字可能比它应该更复杂。这是好消息,下一条规则是最直观的。

您已经听说过并且之前使用过箭头功能的几率。它们是ES6的新版本。它们允许您以更简洁的格式编写函数。

friends.map((friend) => friend.name)

除了简洁之外,箭头函数在this关键字方面具有更直观的方法。与普通函数不同,箭头函数没有自己的函数this。相反,this是确定的lexicallythis根据正常的变量查找规则,这是一种奇特的说法,决定了你的期望。让我们继续我们之前使用的示例。现在,而不必languagesgreet从对象作为单独的,让我们将它们结合起来。

const user = {
  name: 'Tyler',
  age: 27,
  languages: ['JavaScript', 'Ruby', 'Python'],
  greet() {}
}

早些时候,我们假设languages数组总是具有3的长度。通过这样做,我们能够使用硬编码的变量,如l1l2l3。让我们greet现在变得更聪明,并假设它languages可以是任何长度。为此,我们将使用.reduce以创建字符串。

const user = {
  name: 'Tyler',
  age: 27,
  languages: ['JavaScript', 'Ruby', 'Python'],
  greet() {
    const hello = `Hello, my name is ${this.name} and I know`

    const langs = this.languages.reduce(function (str, lang, i) {
      if (i === this.languages.length - 1) {
        return `${str} and ${lang}.`
      }

      return `${str} ${lang},`
    }, "")

    alert(hello + langs)
  }
}

这是更多的代码,但最终结果应该是相同的。当我们调用时user.greet(),我们期望看到Hello, my name is Tyler and I know JavaScript, Ruby, and Python.。可悲的是,有一个错误。你能发现它吗?抓取上面的代码并在控制台中运行它。你会注意到它正在抛出错误Uncaught TypeError: Cannot read property 'length' of undefined。毛。我们唯一使用的地方.length是第9行,所以我们知道我们的错误就在那里。

if (i === this.languages.length - 1) {}

根据我们的错误,this.langauges未定义。让我们逐步完成我们的步骤,找出该this关键字引用的内容,因为它显然不user应该引用它。首先,我们需要查看调用函数的位置。等待?被调用的函数在哪里?该函数正在传递给.reduce我们,所以我们不知道。我们从来没有真正看到我们的匿名函数的调用,因为JavaScript在实现中自己做了.reduce。那就是问题所在。我们需要指定我们希望.reduce在上下文中调用我们传递给的匿名函数user。这种方式this.languages将参考user.languages。如上所述,我们可以使用.bind

const user = {
  name: 'Tyler',
  age: 27,
  languages: ['JavaScript', 'Ruby', 'Python'],
  greet() {
    const hello = `Hello, my name is ${this.name} and I know`

    const langs = this.languages.reduce(function (str, lang, i) {
      if (i === this.languages.length - 1) {
        return `${str} and ${lang}.`
      }

      return `${str} ${lang},`
    }.bind(this), "")

    alert(hello + langs)
  }
}

所以我们已经看到了如何.bind解决这个问题,但这与箭头函数有什么关系。早些时候我说用箭头功能“ this确定了lexicallythis根据正常的变量查找规则,这是一种奇特的说法,决定了你的期望。“

在上面的代码中,遵循您的自然直觉,this匿名函数内部的关键字引用是什么?对我来说,它应该参考user。没有理由创建一个新的上下文只是因为我必须传递一个新的函数.reduce。凭借这种直觉,箭头功能经常被忽视。如果我们重新编写上面的代码,除了使用匿名箭头函数而不是匿名函数声明之外什么都不做,一切都“正常”。

const user = {
  name: 'Tyler',
  age: 27,
  languages: ['JavaScript', 'Ruby', 'Python'],
  greet() {
    const hello = `Hello, my name is ${this.name} and I know`

    const langs = this.languages.reduce((str, lang, i) => {
      if (i === this.languages.length - 1) {
        return `${str} and ${lang}.`
      }

      return `${str} ${lang},`
    }, "")

    alert(hello + langs)
  }
}

再次,因为使用箭头功能,这this是“词汇”确定的原因箭头功能没有自己的功能this相反,就像使用变量查找一样,JavaScript解释器将查看封闭(父)范围以确定this引用的内容。

window绑定

最后是“全能”案例 – 窗口绑定。假设我们有以下代码

function sayAge () {
  console.log(`My age is ${this.age}`)
}

const user = {
  name: 'Tyler',
  age: 27
}

正如我们前面所覆盖,如果你想调用sayAge的背景下user,你可以使用.call.apply.bind如果我们不使用任何这些,而只是sayAge像往常一样调用会发生什么

sayAge() // My age is undefined

不出所料,你得到的是My age is undefined因为this.age未定义。事情变得有些奇怪。什么是真正发生在这里是因为没有什么点的左侧,我们不使用.call.apply.bind,或new关键字,JavaScript是默认this引用的window对象。这意味着如果我们agewindow对象添加一个属性,那么当我们sayAge再次调用我们的函数时,this.age将不再是未定义的,而是它将age是窗口对象上的属性。不相信我?运行此代码,

window.age = 27

function sayAge () {
  console.log(`My age is ${this.age}`)
}

非常粗糙,对吗?这就是为什么第五条规则是window Binding如果没有满足其他规则,则JavaScript将默认this关键字引用该window对象。

从ES5开始,如果启用了“严格模式”,JavaScript将做正确的事情,而不是默认为窗口对象只是将“this”保持为未定义。

'use strict'

window.age = 27

function sayAge () {
  console.log(`My age is ${this.age}`)
}

sayAge() // TypeError: Cannot read property 'age' of undefined

因此,将所有规则付诸实践,每当我this在函数内部看到关键字时,这些都是我为了弄清楚它所引用的内容而采取的步骤。

  1. 查看调用函数的位置。
  2. 点左边有一个物体吗?如果是这样,那就是“this”关键字引用的内容。如果没有,继续#3。
  3. 该函数是使用“call”,“apply”还是“bind”调用的?如果是这样,它将明确说明“this”关键字引用的内容。如果没有,继续#4。
  4. 是否使用“new”关键字调用了该函数?如果是这样,“this”关键字引用由JavaScript解释器创建的新创建的对象。如果没有,继续#5。
  5. 箭头功能中的“this”是什么?如果是这样,则可以在封闭(父)范围内以词汇方式找到其引用。如果没有,继续#6。
  6. 你是在“严格模式”吗?如果是,则“this”关键字未定义。如果没有,继续#7。
  7. JavaScript很奇怪。“this”引用了“window”对象。

原文链接:https://tylermcginnis.com/this-keyword-call-apply-bind-javascript/

发表评论

登录后才能评论