在深入研究 JavaScript 中 this 关键字的细节前,回头看看它起初为何存在是很重要的。this 允许你在不同上下文中重用函数。换句话说,调用函数或方法时,它允许你决定哪个对象应该被关注。接下来讨论的一切都基于此思想,我们想在不同上下文或对象上重用函数或方法。
我们的首要关注点是如何分辨 this 关键字的指向。当试图回答该问题时,你需要问自己的第一也是最重要的问题是 “函数在哪里被调用?”。分辨 this 关键字指向的唯一方式是观察使用 this 的函数在哪儿被调用。
要说明这一点,让我们看一个你已经熟悉的示例,有一个 greet 函数,它接收一个 name,发出一条提示。
function greet (name) {
alert(`Hello, my name is ${name}`)
}如果我问你 greet 会提示什么,你会如何回答?只给出函数定义,无法得知问题答案。要知道 name 是什么,你必须观察函数的调用点。
greet('Tyler')相同思路被用来查找 this 关键字的指向。你甚至可以把 this 看成一个常规函数参数 —— 它将根据调用的方式做改变。
既然知道了辨别 this 指向的第一步是观察函数在哪儿被调用,下一步是什么呢?为了完成接下来的步骤,让我们建立 5 条规则或指导。
- 隐式绑定
- 显式绑定
- new 绑定
- 词法绑定
- window 绑定
隐式绑定
记住,我们的目标是观察使用 this 关键字的函数,并分辨出 this 指向什么。第一也是最常用的规则叫 隐式绑定。它在 80% 的情况下都能告诉你 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
}让我们再看一个类似但稍微高级点的例子。现在除了 name, age, 和 greet 属性,让我们赋予 user 对象一个 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 的方法,它就是为了完成上述功能。
记住以上要点,我们使用下面的代码调用 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,它几乎与 .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 关键字,每当使用它调用函数时,在底层,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 interpreter
created that the this keyword is referencing.
*/
this.name = name
this.age = age
}
const me = new User('Tyler', 27)词法绑定
此刻,我们到达了第四条规则,并且你可能有些困惑。这很正常,JavaScript 中的 this 关键字确实比它应该的更复杂。下面是个好消息,本条规则非常直观。
你很可能听说或使用过箭头函数。它是 ES6 的新语法,允许你以更简洁的格式编写函数。
friends.map((friend) => friend.name)除了简洁,当谈到 this 关键字时,它还更直观。不同于常规函数,箭头函数没有自己的 this。相反,this 由词法确定。有趣的是它就像你期待的那样,遵循正常的变量查找规则。让我们以之前使用的例子继续。现在,让我们把 languages 和 greet 组合到对象中。
const user = {
name: 'Tyler',
age: 27,
languages: ['JavaScript', 'Ruby', 'Python'],
greet() {}
}之前我们假定 languages 数组的长度总是 3。这样做是为了能够使用 l1, l2, 和 l3 这样的硬编码变量。现在让我们使 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.languages 是 undefined。让我们运用步骤清晰地找出 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 由词法决定。有趣的是它就像你期待的那样,遵循正常的变量查找规则。
以上代码中,只是遵循你的自然直觉,匿名函数中的 this 会指向什么。对我来说,它会指向 user。没有必要只因必须为 .reduce 传递函数而创建新的上下文。这种直觉带来了箭头函数常常被忽视的价值。如果我们重写以上代码,不做其它修改只是使用匿名箭头函数取代匿名 function 声明,一切就都正常了。
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 绑定
最后是后备情况 —— window 绑定。假如我们有如下代码
function sayAge () {
console.log(`My age is ${this.age}`)
}
const user = {
name: 'Tyler',
age: 27
}正如之前所述,想在 user 上下文上调用 sayAge,你可以使用 .call, .apply, 或 .bind。如果我们什么都不用,只是像通常那样调用它会发生什么呢?
sayAge() // My age is undefined不出意外,你得到的是 My age is undefined,因为 this.age 没有被定义。这儿情况有点奇怪。真实情形是因为左边没有点,也没有使用 .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}`)
}非常棒,是吗?这就是为何第 5 条规则是 window 绑定。如果不满足其它任何规则,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的指向。如果没有,继续 #3。 - 是使用
.call,.apply, 或.bind调用函数的吗?如果是,它会显式声明this的指向。如果没有,继续 #4。 - 是使用
new关键字调用函数的吗?如果是,this指向 JavaScript 解释器新创建的对象。如果否,继续 #5。 this在箭头函数中吗?如果是,它的指向应该在词法封闭(父)域中。如果否,继续 #6。- 你处于
strict mode吗?如果是,this是 undefined。如果否,继续 #7。 - JavaScript 很奇怪。现在
this指向window对象。
本文译自 ui.dev,译者 LOGI
如有问题请在下方留言,文章转载请注明出处,详细交流请加下方群组!请大佬不要屏蔽文中广告,因为它将帮我分担服务器开支,如果能帮忙点击我将万分感谢。
博主 的BLOG很不错 内容也很好 看好你噢
感谢支持
加油博主,早晚会有流量的
感谢支持,不求流量