this
这位仁兄在我学习 JavaScript 的时候,给我造成了不小困扰。如今 ES6 的箭头函数到来,this
的用法和之前常规函数又不一样了。此文从this
原本的用法开始,用一些简单例子试图把这两者间的不同讲清楚。
this
在常规函数中的用法
代表一个新对象
一般出现在函数被new
调用的时候:
1
const mySundae = new Sundae('Chocolate', ['Sprinkles', 'Hot Fudge']);
在名为Sundae
的构造函数中的this
代表一个新的对象,因为它被new
调用了。
代表一个特定的对象
一般出现在函数被call
/apply
调用的时候:
1
const result = obj1.printName.call(obj2);
上面的代码中,printName()
中的this
指代的是obj2
对象,因为call
方法中的第一个参数指定了this
指代的对象。
代表当前环境对象(context object)
一般出现在此函数是一个对象的方法的时候:
1
data.teleport();
上面的代码中,teleport()
中的this
代表的是data
。
代表全局对象或undefined
一般出现在函数在没有上下文环境的时候被直接调用:
1
teleport();
上面的代码中,teleport()
中出现的this
代表的是全局对象;在严格模式(strict mode)中,则代表undefined
。
综上可知,
this
在常规函数中代表的值,取决于 该函数被如何调用。而在箭头函数中,this
代表的值是该函数所在的上下文环境的值。很拗口,举例说明吧。
让我们把this
放在常规函数中举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 构造函数
function IceCream() {
this.scoops = 0;
}
// 多加1勺冰淇淋
IceCream.prototype.addScoop = function() {
setTimeout(function() {
this.scoops++;
console.log('scoop added!');
}, 500);
};
const dessert = new IceCream();
dessert.addScoop(); // scoop added!
此时,如果对this
的概念不够熟悉的话,你可能以为this.scoops
在 0.5 秒之后已经变成1
了。然而并没有:
1
console.log(dessert.scoops); // 0
因为setTimeout()
没有被new
、call()
、apply()
调用,也没有被上下文对象调用。也就是说,这个setTimeout()
里的函数中的this
指代的是全局对象,而不是实例化的dessert
。
因此,dessert.addScoop();
这行代码做的事情其实是这样的:
- 定义一个名为
scoops
的全局变量,因为没有默认值,所以此时值为undefined
; scoop + 1
也就是:undefined + 1 == NaN
1
console.log(scoops); // NaN
修复这个问题的方法之一,是用闭包:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 构造函数
function IceCream() {
this.scoops = 0;
}
// 多加1勺冰淇淋
IceCream.prototype.addScoop = function() {
const that = this;
setTimeout(function() {
that.scoops++;
console.log('scoop added!');
}, 500);
};
const dessert = new IceCream();
dessert.addScoop(); // scoop added!
此时,在setTimeout()
中,我们没有用它自己的this
,而是把IceCream
构造函数中的this
传给了that
变量,从而避免了setTimeout()
错误地使用全局对象。这样,addScoop()
就可以顺利实现啦:
1
console.log(dessert.scoops); // 1
其实,箭头函数可以直接做到这一点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 构造函数
function IceCream() {
this.scoops = 0;
}
// 多加1勺冰淇淋
IceCream.prototype.addScoop = function() {
setTimeout(() => {
// setTimeout传入箭头函数
this.scoops++;
console.log('scoop added!');
}, 500);
};
const dessert = new IceCream();
dessert.addScoop(); // scoop added!
由于箭头函数被传到setTimeout()
中,所以它继承的是setTimeout()
所在上下文环境中的this
,也就是dessert
,所以addScoop()
可以正常运行:
1
console.log(dessert.scoops); // 1
同理,为了更进一步理解箭头函数,我们可以把addScoop()
也改成箭头函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 构造函数
function IceCream() {
this.scoops = 0;
}
// 多加1勺冰淇淋
IceCream.prototype.addScoop = () => {
// 把 addScoop 改为箭头函数
setTimeout(() => {
// setTimeout传入箭头函数
this.scoops++;
console.log('scoop added!');
}, 500);
};
const dessert = new IceCream();
dessert.addScoop(); // scoop added!
猜猜这次dessert.scoops
的结果是什么?是0
。这跟我们没有加入闭包的常规函数的结果是一样的:
1
console.log(dessert.scoops); // 0
因为箭头函数继承的是上下文环境的this
。因此在addScoop()
方法外部的this
指代的是全局对象。由此导致setTimeout()
中的箭头函数的this
也指代的是全局对象:
1
console.log(scoops); // undefined + 1 == NaN
因此,箭头函数中的
this
,指代的就是此处
,当前的上下文大环境。
更多关于this
的解读,可参考你不知道的 JS