- 原文地址:WTF is this - Understanding the this keyword, call, apply, and bind in JavaScript
- 原文做者:Tyler McGinnis
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:CoolRice
- 校对者:周家未
JavaScript 中最容易被误解的一点就是 this
关键字。在这篇文章中,你将会了解四种规则,弄清楚 this
关键字指的是什么。隐式绑定、显式绑定、new 绑定和 window 绑定。在介绍这些技术时,你还将学习一些 JavaScript 其余使人困惑的部分,例如 .call
、.apply
、.bind
和 new
关键字。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
请记住,这里的目标是查看使用 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
}
复制代码
咱们来看一个相似但稍微高级点的例子。如今,咱们的对象不只要拥有 name
、age
和 greet
属性,还要被添加一个 mother
属性,而且此属性也拥有 name
和 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}`)
}
}
}
复制代码
如今问题变成下面的每一个函数调用会警告什么?
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"
复制代码
第三条判断 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)
复制代码
假如咱们有下面这段代码
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
关键字时,这些就是我为了判断它的引用而采起的步骤。
注:不少小伙伴评论没有讲到箭头函数,因此译者专门写了一篇做为补充,若有须要了解的请挪步也谈箭头函数的 this 指向问题及相关。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。