理解 JavaScript this

14

这是本系列的第 5 篇文章。

还记得上一篇文章中的闭包吗?点击查看文章 理解 JavaScript 闭包

在聊 this 之前,先来复习一下闭包:

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    return function () {
      return 'Hi! My name is ' + this.name;
    }
  }
};

person.sayHi()(); // "Hi! My name is Neil"

上一篇文章说,我们可以把闭包简单地理解为函数返回函数。所以这里的闭包结构是:

// ...
function () {
  return 'Hi! My name is ' + this.name;
}
// ...

但是你有没有发现,这个函数执行的结果是 “Hi! My name is Neil” 。等等,我不是叫 Leo 吗?怎么给我改了个名字?!

我一分析,原来是 this 在其中作祟,且听我慢慢道来这“改名的由来”。

§ this 从何而来

首先,你得确保你已经清楚执行栈与执行上下文的知识。点击查看文章 理解 JavaScript 执行栈

ECMAScript 5.1 中定义 this 的值为执行上下文中的 ThisBinding。而 ThisBinding 简单来说就是由 JS 引擎创建并维护,在执行时被设置为某个对象的引用

在 JS 中有三种情况可以创建上下文:初始化全局环境、eval() 和执行函数。

§ 全局中的 this

var num = 1;

function getName () {
  return "Leo";
}

this.num; // 1
this.getName(); // Leo

this == window; // true

当我们在浏览器中运行这段代码,JS 引擎会将 this 设置为 window 对象。而声明的变量和函数被作为属性挂载到 window 对象上。当然,在严格模式下,全局中 this 的值设置为 undefined。

"use strcit";

var num = 1;
function getName () {
  return "Leo";
}

this.num; // TypeError
this.getName(); // TypeError

this == undefined; // true

开启严格模式后,全局 this 将指向 undefined,所以调用 this.num 会报错。

§ eval() 中的 this

eval() 不被推荐使用,我现在对其也不太熟悉,这里尝试着说一下。初学者可以直接跳到下一节。

结合所查阅的资料,目前我对 eval() 的理解如下:

eval(...) 直接调用,被理解为是一个 lvalue,也有说是 left unchanged,字面理解为余下不变。什么是“余下不变”?我理解为直接调用 eval(...),其中代码的执行环境不变,依旧为当前环境,this 也依旧指向当前环境中的调用对象。

而使用类似 (1, eval)(...) 的代码,被称为间接调用。(1, eval) 是一个表达式,你可以这样认为 (true && eval) 或者 (0 : 0 ? eval)。间接调用的 eval 始终认为其中的代码执行在全局环境,将 this 绑定到全局对象。

var x = 'outer';
(function() {
  var x = 'inner';
  // "direct call: inner"
  eval('console.log("direct call: " + x)');
  // "indirect call: outer"
  (1, eval)('console.log("indirect call: " + x)');
})();

关于 eval(),现在不敢确定,如有错误,欢迎指正。

§ 函数中的 this

◆ 一般情况

首先,我们需要明确的是,在 JS 中函数也属于对象,它可以拥有属性,this 就是函数在执行时获得的属性。一般情况下,在全局环境中直接调用函数,函数中的 this 会在调用时被 JS 引擎设置为全局对象 window(同样在严格模式下为 undefined)。

var name = "Leo";

function getName() {
  var name = "Neil";
  console.log(this); // [object Window]
  return this.name;
}

getName(); // Leo

◆ 作为对象的方法

函数可以作为对象的方法被该对象调用,那么这种情况 this 会被设置为该对象。

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    console.log(this); // person
    return 'Hi! My name is ' + this.name;
  }
};

person.sayHi(); // "Hi! My name is Leo"

当 person 对象调用 sayHi() 方法时,this 被指向 person。

◆ 特殊的内置函数

JS 还提供了一种供开发者自定义 this 的方式,它提供了 3 种方式。

  • Function.prototype.call(thisArg, argArray)
  • Function.prototype.apply(thisArg [, arg1 [, args2, ...]])
  • Function.prototype.bind(thisArg [, arg1 [, args2, ...]])

我们可以通过设置 thisArg 的值,来自定义函数中 this 的指向。

var leo = {
  name: 'Leo',
  sayHi: function () {
    return "Hi! My name is " + this.name;
  }
}

var neil = {
  name: 'Neil'
};

leo.sayHi(); // "Hi! My name is Leo"
​leo.sayHi.call(neil); // "Hi! My name is Neil"

这里,我们通过 call() 将 sayHi() 中 this 的指向绑定为 neil 对象,从而取代了默认 的 this 指向 leo 对象。

关于函数的 call(), apply(), bind() 我将在后面另写一篇文章,敬请期待。

§ this 引起的令人费解的现象

◆ 闭包

通过前面的介绍,我想你对 this 已经有了初步的印象。那么,回到文章开头的问题,this 是怎么改变了我的名字?换句话说,this 在闭包的影响下指向发生了怎样的变动?

再看一下代码:

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    return function () {
      return 'Hi! My name is ' + this.name;
    }
  }
};

person.sayHi()(); // "Hi! My name is Neil"

通过上一篇文章 理解 JavaScript 闭包,函数返回函数会形成闭包。在这种情况下,闭包往往所执行的环境与所定义的环境不一致,而 this 的值却是在执行时决定的。所以,当上面代码中的闭包在执行时,它所在的执行上下文是全局环境,this 将被设置为 window(严格模式下为 undefined)。

怎么解决?我们可以利用 call / apply / bind 来修改 this 的指向。

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    return function () {
      return 'Hi! My name is ' + this.name;
    }
  }
};

person.sayHi().call(person); // "Hi! My name is Leo"

这里利用 call() 将 this 指向 person。OK,我的名字回来了,“Hi! My name is Leo” ^^

当然,我们还有第二种解决方法,闭包的问题就让闭包自己解决。

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    var that = this; // 定义一个局部变量 that
    return function () {
      return 'Hi! My name is ' + that.name; // 在闭包中使用 that
    }
  }
};

person.sayHi()(); // "Hi! My name is Leo"

在 sayHi() 方法中定义一个局部变量,闭包可以将这个局部变量保存在内存中,从而解决问题。

◆ 回调函数

在回调函数中 this 的指向也会发生变化。

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    return 'Hi! My name is ' + this.name;
  }
};

var btn = document.querySelector('#btn');

btn.addEventListener('click', person.sayHi);
// "Hi! My name is undefined"

这里 this 既不指向 person,也不指向 window。那它指向什么?

btn 对象,它是一个 DOM 对象,有一个 onclick 方法,在这里定义为 person.sayHi。

{
  // ...
  onclick: person.sayHi
  // ...
}

所以,当我们执行上面的代码,this.name 的值为 undefined,因为 btn 对象上没有定义 name 属性。我们给 btn 对象自定义一个 name 属性来验证一下。

var btn = document.querySelector('#btn');

btn.name = 'Jackson';

btn.addEventListener('click', person.sayHi);
// "Hi! My name is Jackson"

原因说清楚了,解决方案同样可用过 call / apply / bind 来改变 this 的指向,使其绑定到 person 对象。

btn.addEventListener('click', person.sayHi.bind(person));
// "Hi! My name is Leo"

◆ 赋值

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    return 'Hi! My name is ' + this.name;
  }
};

person.sayHi(); // "Hi! My name is Leo"

var foo = person.sayHi;

foo(); // "Hi! My name is Neil"

当把 person.sayHi() 赋值给一个变量,这个时候 this 的指向又发生了变化。因为 foo 执行时是在全局环境中,所以 this 指向 window(严格模式下指向 undefined)。

同样,我们可以通过 call / apply / bind 来解决,这里就不贴代码了。

§ 别忘了 new

在 JS 中,我们声明一个类,然后 new 一个实例。

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

var her = Person('Angelia');
console.log(her.name); // TypeError

var me = new Person('Leo');
console.log(me.name); // "Leo"

如果我们直接把调用这个函数,this 将指向全局对象,Person 在这里就是一个普通函数,没有返回值,默认 undefined,而尝试访问 undefined 的属性就会报错。

如果我们使用 new 操作符,那么 new 其实会生成一个新的对象,并将 this 指向这个新的对象,然后将其返回,所以 me.name 能打印出 “Leo”。

关于 new 的原理,我会在后面的文章分享,敬请期待。

§ 小结

你看,this 是不是千变万化。但是我们得以不变应万变。

在这么多场景下,this 的指向万变不离其宗:它一定是在执行时决定的,指向调用函数的对象。在闭包、回调函数、赋值等场景下我们都可以利用 call / apply / bind 来改变 this 的指向,以达到我们的预期。

接下来,请期待文章《理解 JavaScript call/apply/bind》。

◆ 文章参考

§ JavaScript 系列文章

理解 JavaScript 闭包

理解 JavaScript 执行栈

理解 JavaScript 作用域

理解 JavaScript 数据类型与变量

Be Good. Sleep Well. And Enjoy.

原文发布在我的公众号 camerae,点击查看

clipboard.png

前端技术 | 个人成长


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

stormstone · 2018年12月19日

666

回复

载入中...