这是本系列的第 5 篇文章。javascript
还记得上一篇文章中的闭包吗?点击查看文章 理解 JavaScript 闭包 。前端
在聊 this 以前,先来复习一下闭包:java
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"
上一篇文章说,咱们能够把闭包简单地理解为函数返回函数。因此这里的闭包结构是:segmentfault
// ... function () { return 'Hi! My name is ' + this.name; } // ...
可是你有没有发现,这个函数执行的结果是 “Hi! My name is Neil” 。等等,我不是叫 Leo 吗?怎么给我改了个名字?!浏览器
我一分析,原来是 this 在其中做祟,且听我慢慢道来这“更名的由来”。闭包
首先,你得确保你已经清楚执行栈与执行上下文的知识。点击查看文章 理解 JavaScript 执行栈 。app
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。spa
"use strcit"; var num = 1; function getName () { return "Leo"; } this.num; // TypeError this.getName(); // TypeError this == undefined; // true
开启严格模式后,全局 this 将指向 undefined,因此调用 this.num 会报错。
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(),如今不敢肯定,若有错误,欢迎指正。
首先,咱们须要明确的是,在 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 种方式。
咱们能够经过设置 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 在闭包的影响下指向发生了怎样的变更?
再看一下代码:
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 来解决,这里就不贴代码了。
在 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》。
Be Good. Sleep Well. And Enjoy.
原文发布在个人公众号 camerae,点击查看。
前端技术 | 我的成长