【译】理解this及call,apply和bind的用法

栏目: JavaScript · 发布时间: 5年前

内容简介:在深入研究JavaScript中我们要看的第一件事是如何判断为了用一个你已经熟悉的例子来证明这一点,比如我们有一个

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

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

为了用一个你已经熟悉的例子来证明这一点,比如我们有一个 greet 函数,它接受了一个alert消息。

function greet (name) {
  alert(`Hello, my name is ${name}`)
}
复制代码

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

greet('Tyler')
复制代码

原理是完全相同的,找出 this 关键字的引用,你甚至,就像你对函数的正常参数一样 - 它会根据函数的调用方式而改变。

现在我们知道为了弄清楚 this 关键字引用的内容,你必须查看函数定义,让我们在实际查看函数定义时建立四个规则来查找。 他们是:

  • 隐式绑定
  • 显式绑定
  • new绑定
  • window绑定

隐式绑定

请记住,这里的目标是能够使用 this 关键字查看函数定义并告诉 this 引用的内容。 执行此操作的第一个也是最常见的规则称为隐式绑定。 我想说绝大多数情况它会告诉你 this 关键字引用了什么。

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

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

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

user.greet()
复制代码

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

在上面的示例中, user 对象是“点的左侧”,这意味着 this 关键字引用 user 对象。 因此,就像在 greet 方法中,JavaScript解释器将 this 更改为 user

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

让我们来看一个类似但稍微更高级的例子。 现在,我们不仅要拥有名称,年龄和问候属性,还要为我们的user对象提供一个mother属性,该属性也有名称和greet属性。

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}`)
    }
  }
}
复制代码

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

user.greet()
user.mother.greet()
复制代码

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

user.greet() // Tyler
user.mother.greet() // Stacey
复制代码

如前所述,绝大多数会有一个“左边的点”的对象。 这就是为什么在弄清楚 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”是每个函数的一个方法,它允许你调用函数,指定调用函数的上下文。

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

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 所做的。 .appall.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 绑定。 如果你不熟悉JavaScript中的 new 关键字,那么每当你使用 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)
复制代码

词法绑定

你已经听说过并且之前使用过箭头函数。 那是ES6的新版本, 以更简洁的格式编写函数。

friends.map((friend) => friend.name)
复制代码

除了简洁之外,箭头函数在涉及 this 关键字时具有更直观的方法。 与普通函数不同,箭头函数没有自己的 this 。 相反,这是词法决定的。 这是一种奇特的方式,说明 this 是根据正常的变量查找规则确定的。 让我们继续我们之前使用的例子。 现在,让我们将它们组合起来,而不是让 languagegreet 与对象分开。

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.. 可悲的是,有一个错误。 你发现l 吗? 抓取上面的代码并在控制台中运行它。 你会注意到它正在抛出错误 Uncaught TypeError: Cannot read property 'length' of undefined. 。 我们只在第9行使用了.length,所以我们知道我们的错误就在那里。

if (i === this.languages.length - 1) {}
复制代码

根据我们的错误, this.langauges 是未定义的。 让我们通过我们的步骤来弄清楚这个关键字引用的原因是什么,它应该是不引用 use 的。 首先,我们需要查看调用函数的位置。 等等? 被调用的函数在哪里? 该函数正被传递给 .reduce ,所以我们不知道。 我们从未真正看到过我们的匿名函数的调用,因为JavaScript在 .reduce 的实现中就是这样做的。 那就是问题所在。 我们需要指定我们希望传递给 .reduce 的匿名函数在用户的上下文中调用。 这样 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 是词法决定的。

在上面的代码中,遵循你的自然直觉, this 关键字引用匿名函数内部会是什么? 对我来说,它应该引用 use 。 没有理由创建一个新的上下文因为我必须将一个新函数传递给 .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
}
复制代码

如前所述,如果你想在 user 的上下文中调用 sayAge ,可以使用 .call.apply.bind 。 如果我们不使用任何这些,而只是像往常一样调用 sayAge 会发生什么

sayAge() // My age is undefined
复制代码

你得到的是, My age is undefined 的,因为 this.age 将是未定义的。 这里的事情变得疯狂了。这里真正发生的是因为点的左边没有任何内容,我们没有使用 .call.apply.bindnew 关键字,JavaScript默认 this 引用 window 对象。 这意味着如果我们将一个 age 属性添加到 window 对象,那么当我们再次调用我们的 sayAge 函数时, this.age 将不再是未定义的,而是它将是 window 对象上的 age 属性。 不相信我? 运行此代码,

window.age = 27

function sayAge () {
  console.log(`My age is ${this.age}`)
}
复制代码

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

在ES5中添加的严格模式中,JavaScript将做正确的事情,而不是默认为window对象只是将“this”保持为未定义。

'use strict'

window.age = 27

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

sayAge() // TypeError: Cannot read property 'age' of undefined
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

算法新解

算法新解

刘新宇 / 人民邮电出版社 / 2016-12-1 / CNY 99.00

本书分4 部分,同时用函数式和传统方法介绍主要的基本算法和数据结构。数据结构部分包括二叉树、红黑树、AVL 树、Trie、Patricia、后缀树、B 树、二叉堆、二项式堆、斐波那契堆、配对堆、队列、序列等;基本算法部分包括各种排序算法、序列搜索算法、字符串匹配算法(KMP 等)、深度优先与广度优先搜索算法、贪心算法以及动态规划。 本书适合软件开发人员、编程和算法爱好者,以及高校学生阅读参考......一起来看看 《算法新解》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

在线进制转换器
在线进制转换器

各进制数互转换器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试