this在普通函数与箭头函数中的不同

作者: shisaq 日期: June 3, 2018

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()没有被newcall()apply()调用,也没有被上下文对象调用。也就是说,这个setTimeout()里的函数中的this指代的是全局对象,而不是实例化的dessert

因此,dessert.addScoop();这行代码做的事情其实是这样的:

  1. 定义一个名为scoops的全局变量,因为没有默认值,所以此时值为undefined
  2. 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