10 分钟带你搞懂 JavaScript的 this 词法

488 阅读4分钟

JavaScript 中的 this 关键字通常用于引用当前执行上下文中的对象,但它的值是动态的,并且在不同的上下文中有不同的值。理解 this 的词法原理是理解 JavaScript 中对象和函数之间关系的关键。

在箭头函数出现之前,每一个新函数都重新定义了自己的 this 值(在构造函数中是一个新的对象;在严格模式下是未定义的;在作为“对象方法”调用的函数中指向这个对象;等等)。以面向对象的编程风格,这样着实有点恼人。😓

以下是 this 词法的几种情况。

全局作用域中的 this 指向什么?

在全局作用域中,this 指向全局对象(在浏览器中为 window 对象,在 Node.js 中为 global 对象)。

代码如下:

console.log(this === window); // true

函数中的 this 指向什么?

在函数内部,this 的指向取决于函数的调用方式。如果函数作为普通函数调用,this 将指向全局对象。如果函数作为对象的方法调用,this 将指向该对象。如果函数使用 call、apply 或 bind 方法调用,this 将指向指定的上下文对象。

代码如下:

1.函数作为普通函数调用。
function foo() {
  console.log(this === window);
}

foo(); // true 

2.函数作为对象的方法调用

var obj = {
  bar: function() {
    console.log(this === obj);
  }
};

obj.bar(); // true

3.函数使用 call、apply 或 bind 方法调用

var baz = {
  qux: function() {
    console.log(this === baz);
  }
};

var quux = baz.qux.bind(baz);
quux(); // true

构造函数中的 this 指向什么?

在使用 new 关键字调用构造函数时,this 将指向新创建的对象实例。

代码如下:

function Person(name) {
  this.name = name;
}

var person = new Person('Alice');
console.log(person.name); // Alice

箭头函数中的 this 指向什么?

箭头函数的 this 绑定在函数定义时的词法环境中,即在函数所在的父级作用域中。因此,在箭头函数内部使用的 this 与它所在的上下文无关。

var obj = {
  foo: function() {
    setTimeout(() => {
      console.log(this === obj);
    }, 0);
  }
};

obj.foo(); // true

在上面的例子中,首先,foo 作为“对象方法”调用,此时 this 指向这个 obj 对象。然后,setTimeout 中定义了一个箭头函数,该箭头函数中的 this 绑定在函数定义时的词法环境中,即在函数所在的父级作用域 foo 中,所以 console.log(this === obj); 输出为 true.

这个复杂示例你学会了吗?

学会了以上这些规则,我们来看个复杂点的示例: 下面这个例子中,我们结合了构造函数、定时器,先不看注释,看看你知道答案吗?

function Person() {
  // 构造函数 Person() 将`this`定义为自身
  this.age = 0;

  setInterval(function growUp() {
    // 在非严格模式下,growUp() 函数将`this`定义为“全局对象”,
    // 这与 Person() 定义的`this`不同,
    // 所以下面的语句不会起到预期的效果。
    this.age++;
  }, 1000);
}

var p = new Person();

我们都知道,定时器 setInterval() 方法可以重复调用一个函数或执行一个代码片段,在每次调用之间具有固定的时间间隔。

上例代码中,setInterval() 方法的第一个参数传入了 growUp 函数,看过基本类型和引用类型区别一文的老铁都知道,这第一个参数传入的实际上是 growUp 函数的内存地址。在 1000 毫秒后再执行函数 growUp 时,this 已经指向了“全局对象”,而非 Person() 定义的this

那么,想要达到预期的效果,我们要如何解决呢?

2 种解决方法

方法一:在 ECMAScript 3/5 里,通过把this的值赋值给一个变量可以修复这个问题。

代码如下:

function Person() {
  var self = this; // 有的人习惯用`that`而不是`self`,
                   // 无论你选择哪一种方式,请保持前后代码的一致性
  self.age = 0;

  setInterval(function growUp() {
    // 以下语句可以实现预期的功能
    self.age++;
  }, 1000);
}

方法二:箭头函数捕捉闭包上下文的this值,所以下面的代码工作正常。

代码如下:

function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // 这里的`this`正确地指向 person 对象
  }, 1000);
}

var p = new Person();

写在最后

总之,this 的值在函数被调用时动态计算,而不是在函数定义时静态计算。在理解 this 的工作原理时,理解 JavaScript 中执行上下文、作用域链、原型链等概念也很重要。

如果这篇文章对你有帮助,请不要吝啬你手中的赞👍!你的鼓励将是我不断分享的动力!😄 前端更多知识及内容请关注如下【同步更新】:

📢 update 同步更新

掘金专栏 | 知乎专栏 | Github | 简书专栏 | CSDN | segmentfault