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

关注公众号 前端开发博客,回复“加群”

加入我们一起学习,天天进步


JavaScript中最容易被误解的一个方面是this关键字。在这篇文章中,将通过学习四个规则来确定此关键字引用的内容。隐式绑定,显式绑定,new绑定和window绑定。在介绍这些时,你还将学习一些其他令人困惑的JavaScript部分,例如.call,.apply,.bind和new关键字。

前言

在深入研究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所做的。.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绑定。如果你不熟悉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

总结

因此,将所有规则付诸实践,每当我在函数内部看到this关键字时,我们可以采用以下步骤弄清楚它所引用的内容。

  1. 查看调用函数的位置

  2. 点左边有一个对象吗?如果是这样,那就是“this”关键字引用的内容。如果没有,继续#3

  3. 该函数是使用“call”,“apply”还是“bind”调用的?如果是这样,它将明确说明“this”关键字引用的内容。如果没有,继续#4

  4. 是否使用“new”关键字调用了该函数?如果是这样,“this”关键字引用由JavaScript解释器创建的新创建的对象。如果没有,继续#5

  5. 你是在“严格模式”吗?如果是,则“this”关键字未定义。如果没有,继续#6

  6. 是的,JavaScript很奇怪。“this”引用了“window”对象。

作者:kovlento

链接:https://juejin.cn/post/6844903841540866056

相关文章

  1. Javascript 里的奇葩知识

  2. 由浅入深,66条JavaScript面试知识点

  3. 20个常用的JavaScript简写技巧

最后

转发文章并关注公众号:前端开发博客,回复 1024,领取前端进阶资料

  1. 回复「电子书」领取27本精选电子书

  2. 回复「加群」加入前端大神交流群,一起学习进步

  3. 回复「JS」获取 JavaScript 精选文章

分享和在看就是最大的支持❤️

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值