this是啥

1
2
3
4
5
6
7
8
9
10
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak:function () {
alert(this.name + ":" + this.motto);
}
}
hero.speak();

运行结果:

1
Hulk:Rahhhhh!!

this的在运行时的值

上面打上的其实是一个典型的面向对象的代码片段。在这个🌰 里,我们用this来指向函数的调用者(一个对象)。但JS不是一个严格的面向对象,所以 this 的行为会不同于其他的语言。说明白点,this 会指向 “运行时上下文”。***上下文是什么鬼?打上另一块码,顺便强行科普一句英文,老铁戳心:Right in the feels!。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var name = "Iron man", motto = "Right in the feels!";
function speak() {
alert(this.name + ":" + this.motto);
}
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak:speak //here we assign the function to the member method
}
hero.speak();
speak();// it reads the two variables in the global context(window)
```
运行结果:

Hulk:Rahhhhh!!
Iron man:Right in the feels!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在上面的代码中,第一个 speak() 是在 hero 的上下文里被调用的,而第二个是在 window。所以 this 会被相应的赋值。证明一下,如果我们直接对 window 调用 speak(),像这样 window.speak() ,我们可以得到和🌰 中第二次调用一样的结果。这是因为 this 解引了全局变量 name。
但是,没人这么写代码吧,所以上面举的并不是现实生活中碰得到的坑。下面我们看一些更真实的🌰 。
## 问题 1 - 回调函数
最常见的导致 "上下文混乱" (这里,我并不太想用上下文切换,所以随便造了个词)是把回调函数当作变量赋值(一般情况是传参)。
```javascript
var name = "Iron man", motto = "Right in the feels!";
function thinkBeforeSpeak(callback) {
setTimeout(callback, 3000);
}
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak :function speak() {
alert(this.name + ":" + this.motto);
}
}
thinkBeforeSpeak(hero.speak);

运行结果:

1
Iron man:Right in the feels!

上面的🌰 中,被测试的那个函数被赋值给其他变量两次,所以函数的上下文又变成 window…然后老铁的心脏又受不了了。

解决办法 1,bind()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var name = "Iron man", motto = "Right in the feels!";
function thinkBeforeSpeak(callback) {
setTimeout(callback, 3000);
}
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak :function speak() {
alert(this.name + ":" + this.motto);
}
}
thinkBeforeSpeak(hero.speak.bind(hero));

运行结果:

1
Hulk:Rahhhhh!!

你可能已经注意到,下面这行是关键点:

1
hero.speak.bind(hero);

这行把 this 给绑定到指定对象,绿巨人 (Hulk)。

解决办法2,apply()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var name = "Iron man", motto = "Right in the feels!";
function thinkBeforeSpeak(callback) {
setTimeout(callback, 3000);
}
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak :function speak() {
alert(this.name + ":" + this.motto);
}
}
thinkBeforeSpeak(hero.speak.apply(hero, []));

运行结果:

1
Hulk:Rahhhhh!!

apply() 和 bind() 差不多,然而 apply() 还有一个额外的参数用于接收给回调函数的传参。

其实还有一个 call(),它的额外参数是一套可变参数(这个词用英语其实会准确些,variable number arguments)。这个函数和 apply() 太像,不举例。

问题 2 - 嵌套函数

另一个常见的坑是在使用嵌套函数(一个函数定义在另一个里面)的时候。这个时候嵌套函数里的 this 不会指向外围函数(假设外围函数是个成员函数)的调用对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var name = "Iron man", motto = "Right in the feels!";
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak :function speak() {
function innervoice() {
alert(this.name + ":" + this.motto);
}
innervoice();
}
}
hero.speak();

运行结果:

1
Iron man:Right in the feels!

我也是。

this 又变成 window 了。好像每次JS引擎一懵*,就把this设置成windows了事。

解决办法,箭头函数=>

=>是 ES6 引进的,它能自动绑定 this:

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = "Iron man", motto = "Right in the feels!";
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak :function speak() {
var fn = () => { alert(this.name + ":" + this.motto); }
fn();
}
}
hero.speak();

运行结果:

1
Hulk:Rahhhhh!!

这次的结果在运行时总算对了。

顺带一提,()=>{…;} 表示一个箭头函数,而且这个函数没参数。

啥?还可以用 that 来解决?还是不要了。

问题3 - 构造函数

在实际用 this 的时候,构造函数也是个天坑。但是要理解这个问题需要对 JS 的面向对象基础有一定了解。