JavaScript: 理解this是很容易

2021-04-04

image-20210404183144709

JavaScript: 理解this是很容易

引言

本文将介绍JavaScript的另一个基本但同样被误解的方面,称为this关键字。

好吧,让我们开始吧。在本文的结尾,您应该对以下内容有更好的了解,

在JavaScript中this是什么。

  • 如何使this听起来比以往更少混乱
  • this的规则及其用法。
  • 重要的是,this很容易!

冗长的文章阅读提醒

与本系列的其他文章不同,该文章将有些冗长。看完几本教程后,我觉得应该将各个方面联系起来,以很好地理解this一概念。例如,call => apply => bind的概念与理解this关键字非常相关。我们需要一起讨论。

我可以将其分解为多篇文章,但是最好将它们放在一起,因为这些概念是相互关联的。

带上您最喜欢的饮料,放松身心,然后开始阅读。我相信您一定会喜欢的。

this是什么?

this是JavaScript中的关键字,它的存在是为了允许我们,

  • 在不同的上下文中重用函数。
  • 标识在调用方法时要关注的对象。

请记住,在JavaScript中创建执行上下文时,它会创建一个称为this的特殊实体。

在全局执行上下文中,这等于全局对象window

在函数执行上下文中,默认情况下,this等于window对象,但是行为根据称为绑定的现象而改变。不要将其与JavaScript中的bind()方法混淆。 bind()方法只是绑定内容的另一种方法。还有更多。我们将在一段时间内看到它,并且this 90%的用法都围绕它。

this的规则

理解函数的this关键字可能具有挑战性,因为它在JavaScript中的行为不同于其他语言。当涉及到这一点时,要问的重要问题是,

该函数在哪里调用?在调用函数之前,我们不知道this关键字中包含什么。

this的用法可以分为四个不同的绑定方面。

隐私绑定(Implicit binding)

隐式绑定涵盖了我们使用this关键字处理的大多数用例。在隐式绑定中,在调用时与函数相邻的dot(.)运算符的左侧确定this对象绑定到什么。

这里有一个例子,

1
2
3
4
5
6
7
8
9
var user = {
name: 'GreenRoots',
address: 'HashNode',
getName: function() {
console.log(this.name);
}
};

user.getName();

说明:在上面的示例中,this绑定到user对象,与函数getName()相邻的dot(.)运算符的左侧是user对象。因此,this.name将在控制台中记录GreenRoots

让我们再举一个例子来更好地解释这个概念,

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function decorateLogName(obj) {
obj.logName = function() {
console.log(this.name);
}
};

var tom = {
name: 'Tom',
age: 7
};

var jerry = {
name: 'jerry',
age: 3
};

decorateLogName(tom);
decorateLogName(jerry);

tom.logName();
jerry.logName();

说明:在上面的示例中,我们有两个对象,tomjerry。我们通过附加一个名为logName()的方法来修饰(增强)这些对象。

请注意,当我们调用tom.logName()时,与函数logName()相邻的dot(.)运算符的左侧是tom对象。因此,this绑定到tom对象,并记录值tomthis.name等于tom)。调用jerry.logName()时,同样适用。

够清楚了吗?好!现在,让我们以JavaScript类为例,它只是一个函数,但是我们可以创建它的不同实例,

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var Cartoon = function (name, age, friend) {
return {
name: name,
age: age,
logName: function() {
console.log(this.name);
},
friend: {
name: friend,
logName: function() {
console.log(this.name);
}
}
}
};

var tom = Cartoon('Tom', 7, 'Jerry');
tom.logName(); // Should print 'Tom'
tom.friend.logName(); // Should print 'Jerry'

说明:在这里,我们有一个名为Cartoon的类。卡通有一个名为logName()的方法。它也有一个叫做friend的属性,它是一个对象。为了使事情有些棘手,对象friend也有一个名为logName()的方法。

实例化Cartoon类后,我们得到一个名为tom的实例,该实例传递名称,即Tomage为7,而friend的名为Jerry

调用tom.logName()时,与函数logName()相邻的dot(.)运算符的左侧是tom,并且this绑定tom。因此,这里的this.name指向tom的名字,我们之前以tom的名字传递它。

调用tom.friend.logName()时,与函数logName()相邻的dot(.)运算符的左侧为friend。因此,this指向对象friendthis.name是对象friend的名字,即Jerry

我希望我们充分理解隐式绑定的规则和逻辑以应用和实现它。让我们继续前进。

显式绑定(Explicit binding)

我们知道,JavaScript创建了一个环境来执行我们编写的代码。该环境包含的内容超出了我们编写的实际代码。

在创建阶段,它负责为变量,函数,对象等创建内存。最后,在执行阶段执行代码。这种特殊的环境称为JavaScript执行上下文。您可以从这里阅读更多有关它的信息。

JavaScript应用程序中有许多这样的环境(执行上下文)。每个执行上下文彼此独立地操作。但是有时,我们可能希望使用从一个执行上下文到另一个执行上下文的东西。这就是显式绑定起作用的地方。我们可以将内容从一个上下文绑定到另一环境的上下文中,以使用this上下文执行。

在显式绑定中,我们可以在对象的执行上下文之外调用带有对象的函数。

共有三种非常规方法,call()apply()bind()有助于实现显式绑定。

call()方法

使用call()方法,必须将调用函数的上下文作为参数传递给call()。让我们看一个例子,

例如:

1
2
3
4
5
6
7
8
9
var getName = function() {
console.log(this.name);
}
var user = {
name: 'GreenRoots',
address: 'HashNode'
};

getName.call(user);

说明:我们在这里看到的是,getName()的函数上调用了call()方法。 getName()函数仅仅记录this.name。但是,这里的this是什么?这取决于传递给call()方法的内容。

在这里,this将绑定到user对象,因为我们已经将user作为参数传递给call()方法。因此,this.name应该记录user对象的name属性的值,即GreenRoots

在上面的示例中,我们仅将一个参数传递给call()。但是,如果需要,我们可以将多个参数传递给call()。让我们再举一个例子来了解这一点,

例如

1
2
3
4
5
6
7
8
9
10
var getName = function(hobby1, hobby2) {
console.log(this.name + ' likes ' + hobby1 + ' , ' + hobby2);
}
var user = {
name: 'Tapas',
address: 'Bangalore'
};

var hobbies = ['Swimming', 'Blogging'];
getName.call(user, hobbies[0], hobbies[1]);

说明:注意,我们在这里在call()方法中传递了另外两个参数。第一个参数必须是必须使用该函数调用的对象上下文。其他参数可能只是要使用的值。在这里,我将SwimmingBlogging作为两个参数传递给getName()函数。 您在这里注意到痛点了吗?在使用call()的情况下,参数必须一个接一个地传递,这不是一种明智的处理方式!这就是我们的下一个方法apply()讲解的。

apply() 方法

可以通过另一种名为apply()的替代方法来解决将参数传递给call()方法的麻烦方法。它与call()相同,但允许更方便地传递参数。看一看,

例如

1
2
3
4
5
6
7
8
9
10
var getName = function(hobby1, hobby2) {
console.log(this.name + ' likes ' + hobby1 + ' , ' + hobby2);
}
var user = {
name: 'Tapas',
address: 'Bangalore'
};

var hobbies = ['Swimming', 'Blogging'];
getName.apply(user, hobbies);

说明:如您在此处看到的,我们可以将数组作为参数传递,这比逐个传递要方便得多。

当您只传递一个值参数或没有值参数时,请使用call()。当您有多个要传递的值参数时,请使用apply()

bind() 方法

call()方法通过传递this上下文来调用该函数。 bind()方法与call()类似,但是bind()不会直接调用该函数,而是返回一个全新的函数,我们可以调用它。

例如

1
2
3
4
5
6
7
8
9
10
11
12
var getName = function(hobby1, hobby2) {
console.log(this.name + ' likes ' + hobby1 + ' , ' + hobby2);
}
var user = {
name: 'Tapas',
address: 'Bangalore'
};

var hobbies = ['Swimming', 'Blogging'];
var newFn = getName.bind(user, hobbies[0], hobbies[1]);

newFn();

说明:如上所述,getName.bind()不会调用函数getName()。它返回一个新函数newFn,我们可以像newFn()那样调用它。

new 绑定

使用new关键字创建一个构造函数。这是一个构造函数的例子,

1
2
3
4
5
6
7
var Cartoon = function(name, animal) {
this.name = name;
this.animal = animal;
this.log = function() {
console.log(this.name + ' is a ' + this.animal);
}
};

我们可以使用new关键字创建对象,

1
2
var tom = new Cartoon('Tom', 'Cat');
var jerry = new Cartoon('Jerry', 'Mouse');

构造函数的new绑定规则规定,当使用new关键字调用函数时,函数内的this关键字绑定到正在构造的新对象。

听起来复杂吗?好吧,让我们分解一下。走这条线,

1
var tom = new Cartoon('Tom', 'Cat');

在这里,函数Cartoon是通过new关键字调用的。因此,this将绑定到此处创建的新对象tom

全局对象绑定

该代码执行的输出是什么?this绑定到这里是什么?

1
2
3
4
5
6
7
var sayName = function(name) {
// 'use strict';
console.log(this.name);
};

window.name = 'Tapas';
sayName();

如果this关键字未通过上述任何绑定(隐式显式new绑定)解析,则this绑定到windowglobal)对象。

箭头函数,没有绑定吗?

ES6引入了箭头函数,这些功能不提供自己的this绑定。到目前为止,我们已经看到,在常规函数中,this关键字表示调用该函数的对象,该对象可以是window,文档,用户定义的对象或任何其他内容。

使用箭头函数时,this关键字始终代表定义了箭头函数的对象。

箭头函数使用词法作用域,“ this”是指其当前的作用域。箭头函数不绑定自己的作用域,而是从父级继承它。

时间示例。让我们看看它是的工作。

1
2
3
4
5
6
7
8
9
var testHobbies = {
hobbies: ['Cricket', 'Football', 'Blogging'],
name: 'Alex',
logHobbies() {
this.hobbies.forEach((elem) => {
console.log(`${this.name} knows ${elem}`);
});
}
}

在这里,logHobbies()方法遍历这些兴趣爱好并将其记录到控制台中。注意,我们在forEach中使用箭头函数。箭头函数内部的this将绑定到对象testHobbies,因为箭头函数没有此绑定,并且始终绑定到父对象。

因此,调用testHobbies.logHobbies()将正确记录为:

1
2
3
Alex knows Cricket
Alex knows Football
Alex knows Blogging

现在让我们改变一下。请注意我在下面所做的修改。 for-each使用常规函数代替箭头函数。

1
2
3
4
5
6
7
8
9
var testHobbies = {
hobbies: ['Cricket', 'Football', 'Blogging'],
name: 'Alex',
logHobbies() {
this.hobbies.forEach(function(elem){
console.log(`${this.name} knows ${elem}`);
});
}
}

您怎么看,this将绑定到forEach中?它不是箭头函数。它是一个常规函数,并且具有自己的执行上下文。在该执行上下文中,找不到一个叫name。因此,this.nameundefined

因此将输出

1
2
3
undefined knows Cricket
undefined knows Football
undefined know Blogging

我们将在以后有关Scope闭包的文章中更详细地介绍它。

使用严格模式和this

通常,在全局作用域内,this关键字指的是window对象,

1
2
3
<script>
console.log(this); //returns window object.
</script>

同样在JavaScript严格模式下,全局作用域内的this关键字返回window对象。但是,它在函数作用域内的行为有所不同。

看下面例子

1
2
3
4
5
6
7
8
9
10
11
<script>
"use strict;"
console.log(this);

function testThis() {
"use strict";
console.log('testThis', this);
}

testThis();
</script>

它将在控制台中记录以下输出,

1
2
Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
testThis undefined

总结

是的,this很容易理解!但是同时,理解this规则和用法可能会很困难。当我们专注于以下问题时,我们将更好地理解this一点:在哪里调用函数? 在大多数情况下,将与隐式绑定一起使用。显式绑定将与call()apply()bind()一起使用。在许多基于JavaScript的框架(如Reactjs,Angular等)中,我们使用箭头函数。 只需注意,只要您理解并实践了这些规则,我相信您会同意this确实很容易使用!

后记

翻译来自JavaScript: this is easy and what do you need to know about it!