前言

前段时间公司一位前端实习生过来请教js中的this指向问题。这个问题确实是前端开发中经常会遇到的问题。但是这个问题比较杂,回答起来有些费劲。还好,最后能够让他弄清楚这块知识点。这也让我有写这篇博客的想法。

简单的说,传统函数中的this指向的执行该函数时运行环境,ES6箭头函数中的this则指向的定义该函数时的运行环境。

传统函数中的this

我们来看几个🌰:

1
2
3
4
5
6
7
8
9
10
var a = 1;
function test() {
var a = 2;
console.log(this.a);
}
test(); // 1
var context = { a: 3, b: 2 };
test.call(context); // 3

第一次执行函数test时,是在全局的环境下,所以this指向的是windowthis.a也就为全局定义的a了。
第二次执行test时,我们通过call方法来改变了test函数的执行环境,这时的this指向的就是context对象了,所以也就打印出了3
从中我们可以看出,函数中的this会随着执行时的上下文环境的改变而改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a = 0;
function Test() {
this.a = 1;
this.c = function () {
console.log(this.a);
}
this.d = function () {
setTimeout(function () {
console.log(this.a);
}, 100);
}
}
var testObj = new Test();
testObj.c(); // 1
testObj.d(); // 0

这里我们定义了一个构造函数Test,然后通过它实例化了testObj对象,执行testObjc方法,由于c方法是在testObj的环境下运行的,所以这里的this执行的是testObj这个对象,也就打印出了内部属性a
类似的d方法一样的也是在testObj的环境下运行的,结果却打印出了0,也就是全局的a变量,这是为什么?
其实这里涉及到了异步的知识,d方法内部是执行了一个定时器,0.1s后运行一个匿名函数,打印出this.ad函数是在testObj的环境下执行的,可是这个的匿名函数根据异步函数的运行原理可以知道,它是在全局的环境下运行的。所以这里的this也就指向的是window(关于异步,后面会专门写一篇博客谈谈)

我们如何实现在这个匿名函数中打印出testObj的a属性?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 方法 1
this.d = function () {
var that = this;
setTimeout(function () {
console.log(that.a);
}, 100);
}
// 方法 2
this.d = function () {
setTimeout((function (that) {
console.log(that.a);
})(this), 100);
}
// 方法 3
this.d = function () {
setTimeout((function () {
console.log(this.a);
}).call(this), 100);
}

方法一是平时最常用,将异步执行的函数中的this替换成普通变量that,并提前将需要的运行环境赋值给that,这样就不存在异步函数中this指向不明确的问题了。
方法二和方法一类似,只是通过传参的方式,将需要的运行环境传入。
方法三则是通过call方法改变匿名函数的运行环境。

有人会问直接运行下面代码,会导致什么现象

1
2
3
4
5
6
7
8
9
10
11
12
13
function Test() {
this.a = 1;
this.c = function () {
console.log(this.a);
}
this.d = function () {
setTimeout(function () {
console.log(this.a);
}, 100);
}
}
Test();

主要弄清楚了前面说的this的指向问题,那么这就不存在什么困难了。这就是在全局的环境下执行Test函数,那么this指向的就是window。所以运行后的结果就是在window对象上绑定了abc三个属性。

有同学会对testObj的产生过程表示疑惑,var testObj = new Test()在我的理解用代码表示就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Test() {
var _this = new Object();
_this.a = 1;
_this.c = function () {
console.log(this.a);
}
_this.d = function () {
setTimeout(function () {
console.log(this.a);
}, 100);
}
return _this;
}
var testObj = Test();

箭头函数中的this

随着ES6的出现,给我们带来了很多特性。其中的箭头函数是其中一个比较重要的特性,极大的简化了我们的代码。并且很好的解决了函数中this的指向问题。
可以将下面的代码和上面同一部分的代码对比着看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a = 0;
function Test() {
this.a = 1;
this.c = function () {
console.log(this.a);
}
this.d = function () {
setTimeout(() => {
console.log(this.a);
}, 100);
}
}
var testObj = new Test();
testObj.c(); // 1
testObj.d(); // 1

其中的改动就是把定时器中匿名函数改成了箭头函数,我们发现d方法的执行结果有了变化。这是因为箭头函数中的this执行的始终是定义这个函数时的上下文,不会随着执行的环境改变而改变。这个箭头函数是在testObj的环境下定义的,所以这里的this.a也就是内部属性a了。

有人会对定义时的环境有点不太清楚

1
2
3
4
5
6
7
8
9
10
11
12
13
function Test() {
this.a = 1;
this.c = function () {
console.log(this.a);
}
this.d = function () {
console.log(this); // a处
setTimeout(() => {
console.log(this) // b处
console.log(this.a);
}, 100);
}
}

其实箭头函数内的this(b处)和 d函数内部的this(a处)是一样的,而a处的this是根据d的运行环境确定的。

所以就有了下面的运行结果:

1
testObj.d.call({a: 100}); // 100