this -- Javascript基础探究篇(8)

476 阅读4分钟

this应该是js中最为复杂的机制之一。搞懂this某种程度上意味着一次重生。

我们先通过一段代码来看看this有多么让人琢磨不透:

function foo(num) {
  console.log(`foo: ${num}`);
  this.count++;
}

foo.count = 0;

foo(1);
foo(2);
console.log(foo.count); // ?

此时的foo.count的值是多少呢?答案是0。

显然,我们调用了两次foo函数,所以this.count++也肯定运行了两次。但是最后输出的this.count却还是0。那么函数里面的this到底实际指向的什么呢?既然代码能够正确的运行,那么this.count到底又是哪一个count呢?

如果我们最后console.log加上一个断点,看看各个作用域的情况,我们会发现此时的全局作用域多了一个count变量,并且它的值此时为NaN

当前的执行模式是非严格模式

this绑定规则

首先我们要抛出一个结论:函数(箭头函数除外)内部的this是在函数被调用时绑定的,它的值取决于函数的调用方式。可以分为以下情况:

默认绑定

即独立函数调用。如果在严格模式下,this绑定到undefined;非严格模式下,绑定到全局对象。

global.a = 2;
function foo() {
  console.log(this.a);
}

foo(); // 2

foo此时被独立调用,在非严格模式下,this绑定到global,所以能输出2。如果是严格模式:

"use strict";
function foo() {
  console.log(this.a);
}

foo(); // TypeError: Cannot read property 'a' of undefined

this绑定到undefined。所以无法获取a变量。

现在我们再回头看看最开始的例子。因为foo是非严格模式下的独立调用,所以this指向全局对象。此时会在全局对象创建一个变量a,在未赋值的情况下,a的值是undefinedundefined + 1的结果就是NaN,第二次就是Nan + 1所以结果仍是NaN

隐式绑定

如果是通过类似于obj.fn()的形式调用时,那么fn内部的this会绑定到obj对象上。

function foo() {
  console.log(this.a);
}

const obj = {
  a: 2,
  foo,
};

obj.foo(); // 2; 此时foo中的this会被绑定到obj对象

需要注意的是调用函数的this会绑定到对象属性引用链只有最后一层:

function foo() {
  console.log(this.a);
}

const obj2 = {
  a: 2,
  foo,
};

const obj1 = {
  a: 1,
  obj2,
};

obj1.obj2.foo(); // 2; 此时foo中的this会被绑定到obj2对象

思考以下绑定情况:

function foo() {
  console.log(this.a);
}

const obj = {
  a: 2,
  foo,
};

const bar = obj.foo;

bar(); // undefined

我们通过将obj.foo赋值给bar,然后调用bar。此时bar函数属于独立调用,所以会使用默认绑定规则。

显示绑定

也称为硬绑定,即使用call,apply或者bind的方式显示指定this所绑定的对象。

function foo() {
  console.log(this.a);
}

const obj = {
  a: 2,
};

foo.call(obj); // 显示绑定this为obj

需要注意:如果把undefined或者null作为硬绑定的this对象。这些值会被忽略,最终还是应用默认绑定规则。

绑定优先级

不同的绑定方式优先级:显示绑定 > 隐式绑定 > 默认绑定

function foo() {
  console.log(this.a);
}

const obj = {
  a: 2,
  foo,
};

obj.foo(); // 2
obj.foo.call({ a: 3 }); // 3

箭头函数

箭头函数不会按照以上的三种规则绑定this,而是根据外层作用域来决定this。即当箭头函数被声明时,它所在的作用域的this是什么,那么箭头函数内部的this就是什么。并且不会再发生改变(这就是为什么箭头函数无法作为构造函数)。

function foo() {
  return () => {
    console.log(this.a);
  };
}

const obj = { a: 2 };
const bar = foo.call(obj);
bar.call({ a: 3 }); // 2

在调用foo函数时,会在它内部声明一个箭头函数,而通过foo.call({ a: 2 })的形式调用函数,那么foo内部的this绑定为obj。此时声明的箭头函数会捕获此时的this。所以箭头函数内部的this也会绑定到obj。并且箭头函数的this无法被修改,所以后续对箭头函数的调用,其内部使用的this始终是obj

箭头函数常用于回调函数:

function foo() {
  setTimeout(() => {
    console.log(this.a); // this继承于foo
  }, 1000);
}

foo.call({ a: 2 }); // 2

如果我们将箭头函数替换为普通的匿名函数,结果将会为undefined。因为setTimeout的回调函数在真正调用时其实是独立调用,所以会应用默认绑定。而箭头函数在执行foo时就已经确定是{a: 2}这个对象,不会再修改。