【译】理解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
是根据正常的变量查找规则确定的。 让我们继续我们之前使用的例子。 现在,让我们将它们组合起来,而不是让 language
和 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..
可悲的是,有一个错误。 你发现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
, .bind
或 new
关键字,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 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 深入理解 Typescript 高级用法
- 【css基础】如何理解transform的matrix()用法
- Mybatis的一级缓存和二级缓存的理解以及用法
- AWK 的用法
- AWK基础用法
- UniversalImageLoader的用法总结
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。