深入JS之this

从《深入JS中的作用域和执行上下文》中可以得知,当JS引擎执行一段JS可执行代码时,会创建对应的执行上下文。而在创建执行上下文的过程中,要经历三个步骤:创建变量对象、建立作用域链以及确定this指向。

由上,首先记住一句话,this,是在创建执行上文时被确定的。而执行上下文,从《深入作用域链》中可以知道是在函数被激活(调用)的时候才被创建的。

在绝大多数情况下,函数的调用方式决定了this的值。this不能在执行期间被赋值,并且在每次函数被调用时this的值也可能会不同。 ——引自MDN

1
2
3
4
5
6
7
8
9
10
11
var a = 10;
var obj = {
a: 20
}

function fn() {
this = obj; // 这句话试图修改this,运行后会报错
console.log(this.a);
}

fn();

全局上下文

无论是否在严格模式下,在全局执行上下文中(在任何函数体外部)this 都指代全局对象。

1
2
3
4
5
6
7
8
9
// 在浏览器中, window 对象同时也是全局对象:
console.log(this === window); // true

a = 37;
console.log(window.a); // 37

this.b = "MDN";
console.log(window.b) // "MDN"
console.log(b) // "MDN"

函数上下文

在函数内部,this的值取决于函数被调用的方式。

1. 独立函数中的 this

1
2
3
4
5
6
7
8
9
10
// 非严格模式,且this的值不是由该调用设置,所以默认指向全局对象

function f1(){
return this;
}
//在浏览器中:
f1() === window; //在浏览器中,全局对象是window

//在Node中:
f1() === global;
1
2
3
4
5
6
7
8
// 严格模式下,this将保持他进入执行上下文时的值,下面的this默认为undefined

function f2(){
"use strict"; // 这里是严格模式
return this;
}

f2() === undefined; // true

所以,在严格模式下,如果 this 没有被执行上下文(execution context)定义,那它将保持为 undefined。

再来看一道题:

1
2
3
4
5
6
7
8
9
10
11
var a = 20;
var foo = {
a: 10,
getA: function() {
return this.a;
}
}
console.log(foo.getA()); // 10

var test = foo.getA;
console.log(test()); // 20

foo.getA() 中, getA 是调用者,他不是独立调用,被对象 foo 所拥有,因此它的 this 指向了 foo 。而 test() 作为调用者,尽管他与 foo.getA 的引用相同,但是它是独立调用的,因此 this 指向 undefined ,在非严格模式,自动转向全局 window 。

结论:如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。

2. 对象的方法中的 this

  • 作为普通对象的方法调用
    1
    2
    3
    4
    5
    6
    7
    8
    var o = {
    prop: 37,
    f: function() {
    return this.prop;
    }
    };

    console.log(o.f()); // 37

这样的调用方式,不受函数定义位置或者方式的影响。比如下面,我们也可以先定义函数,再将其附属到对象o上。

1
2
3
4
5
6
7
8
9
var o = {prop: 37};

function independent() {
return this.prop;
}

o.f = independent;

console.log(o.f()); // 37

这表明函数是从o的f成员调用的才是重点。并且:

this 的绑定只受最靠近的成员引用的影响。

1
2
3
4
5
6
7
8
var name = "windowName";
var a = {
name: "innerName",
fn: function() {
console.log(this.name); // innerName
}
}
window.a.fn();
  • 原型链中的this
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var o = {
    f: function() {
    return this.a + this.b;
    }
    };
    var p = Object.create(o);
    p.a = 1;
    p.b = 4;

    console.log(p.f()); // 5

对象p没有属于它自己的f属性,它的f属性继承自它的原型。查找p的f属性过程首先从 p.f 的引用开始,所以函数中的 this 指向p。也就是说,因为f是作为p的方法调用的,所以它的this指向了p。

结论:如果该方法存在于一个对象的原型链上,那么this指向的是调用这个方法的对象,就像该方法在对象上一样。

  • getter 与 setter 中的 this
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function sum() {
    return this.a + this.b + this.c;
    }

    var o = {
    a: 1,
    b: 2,
    c: 3,
    get average() {
    return (this.a + this.b + this.c) / 3;
    }
    };

    Object.defineProperty(o, 'sum', {
    get: sum,
    enumerable: true,
    configurable: true});

    console.log(o.average, o.sum); // 2, 6

结论:用作 getter 或 setter 的函数都会把 this 绑定到设置或获取属性的对象。

3. 构造函数中的 this

当一个函数用作构造函数时(使用new关键字),它的this被绑定到正在构造的新对象

1
2
3
4
5
6
function C() {
this.a = 37;
}

var o = new C();
console.log(o.a); // 37

在这里说一下new的过程:

  1. var obj = {}; // 创建一个空对象
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性和方法)
  4. 返回新对象

!!! 虽然构造器返回的默认值是this所指的那个对象,但它仍可以手动返回其他的对象(如果返回值不是一个对象,则返回this对象)。比如像下面:

1
2
3
4
5
6
7
function C2(){
this.a = 37;
return {a:38};
}

o = new C2();
console.log(o.a); // 38

4. 箭头函数中的 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100); // s1: 3
setTimeout(() => console.log('s2: ', timer.s2), 3100); // s2: 0

Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。

结论:箭头函数的 this 始终指向函数定义时的 this,而非执行时。 this 指向的固定化,并不是因为箭头函数内部有绑定 this 的机制,实际原因是箭头函数根本没有自己的 this ,导致内部的 this 就是外层代码块的 this 。正是因为它没有 this ,所以也就不能用作构造函数。