[译] this(他喵的)究竟是什么 — 理解 JavaScript 中的 this、call、apply 和 bind

JavaScript 中最容易被误解的一点就是 this 关键字。在这篇文章中,你将会了解四种规则,弄清楚 this 关键字指的是什么。隐式绑定、显式绑定、new 绑定和 window 绑定。在介绍这些技术时,你还将学习一些 JavaScript 其余使人困惑的部分,例如 .call.apply.bindnew 关键字。javascript

视频

正文

在深刻了解 JavaScript 中的 this 关键字以前,有必要先退一步,看一下为何 this 关键字很重要。this 容许复用函数时使用不一样的上下文。换句话说,“this” 关键字容许在调用函数或方法时决定哪一个对象应该是焦点。 以后讨论的全部东西都是基于这个理念。咱们但愿可以在不一样的上下文或在不一样的对象中复用函数或方法。前端

咱们要关注的第一件事是如何判断 this 关键字的引用。当你试图回答这个问题时,你须要问本身的第一个也是最重要的问题是“这个函数在哪里被调用?”。判断 this 引用什么的 惟一 方法就是看使用 this 关键字的这个方法在哪里被调用的。java

用一个你已经十分熟悉的例子来展现这一点,好比咱们有一个 greet 方法,它接受一个名字参数并显示有欢迎消息的警告框。android

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

若是我问你 greet 会具体警告什么内容,你会怎样回答?只给出函数定义是不可能知道答案的。为了知道 name 是什么,你必须看看 greet 函数的调用过程。ios

greet('Tyler')
复制代码

判断 this 关键字引用什么也是一样的道理,你甚至能够把 this 当成一个普通的函数参数对待 — 它会随着函数调用方式的变化而变化。git

如今咱们知道为了判断 this 的引用必须先看函数的定义,在实际地查看函数定义时,咱们设立了四条规则来查找引用,它们是github

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

隐式绑定

请记住,这里的目标是查看使用 this 关键字的函数定义,并判断 this 的指向。执行绑定的第一个也是最多见的规则称为 隐式绑定。80% 的状况下它会告诉你 this 关键字引用的是什么。后端

假如咱们有一个这样的对象数组

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

如今,若是你要调用 user 对象上的 greet 方法,你会用到点号。bash

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

咱们来看一个相似但稍微高级点的例子。如今,咱们的对象不只要拥有 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 将会指向传递给 call 的第一个参数。

这就是第 2 条规则的基础(显示绑定),由于咱们明确地(使用 .call)指定了 this 的引用。

如今让咱们对 greet 方法作一点小小的改动。假如咱们想传一些参数呢?不只提示他们的名字,还要提示他们知道的语言。就像下面这样

function greet (lang1, lang2, lang3) {
  alert(`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`)
}
复制代码

如今为了将这些参数传递给使用 .call 调用的函数,你须要在指定上下文(第一个参数)后一个一个地传入。

function greet (lang1, lang2, lang3) {
  alert(`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`)
}

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 会在函数中为你自动展开。

那么如今用 .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 (lang1, lang2, lang3) {
  alert(`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`)
}

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) {
  /*
    JavaScript 会在底层建立一个新对象 `this`,它会代理不在 User 原型链上的属性。
    若是一个函数用 new 关键字调用,this 就会指向解释器建立的新对象。
  */

  this.name = name
  this.age = age
}

const me = new User('Tyler', 27)
复制代码

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 name is undefined,由于 this.age 是 undefined。事情开始变得神奇了。实际上这是由于点的左侧没有任何东西,咱们也没有用 .call.apply.bind 或者 new 关键字,JavaScript 会默认 this 指向 window 对象。这意味着若是咱们向 window 对象添加 age 属性并再次调用 sayAge 方法,this.age 将再也不是 undefined 而且变成 window 对象的 age 属性值。不相信?让咱们运行这段代码

window.age = 27

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

很是神奇,不是吗?这就是第 4 条规则为何是 window 绑定 的缘由。若是其它规则都没知足,JavaScript就会默认 this 指向 window 对象。


在 ES5 添加的 严格模式 中,JavaScript 不会默认 this 指向 window 对象,而会正确地把 this 保持为 undefined。

'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” 就是 undefined,若是不是,继续第 6 步。
  6. JavaScript 很奇怪,“this” 会指向 “window” 对象。

注:不少小伙伴评论没有讲到箭头函数,因此译者专门写了一篇做为补充,若有须要了解的请挪步也谈箭头函数的 this 指向问题及相关

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索