这是本系列的第 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》。
◆ 文章参考
Understand JavaScript’s “this” With Clarity, and Matser It | Richard Bovell
How does the “this” keyword work | Stack Overflow
ECMAScript Language Specification – $11.1.1 The This Keyword
(1, eval)(‘this’) vs eval(‘this’) in JavaScript | Stack Overflow
§ JavaScript 系列文章
理解 JavaScript 闭包
理解 JavaScript 执行栈
理解 JavaScript 作用域
理解 JavaScript 数据类型与变量
Be Good. Sleep Well. And Enjoy.
原文发布在我的公众号 camerae。
前端技术 | 个人成长