这是前端面试题系列的第 4 篇,你可能错过了前面的篇章,能够在这里找到:javascript
在前端的面试中,常常会问到有关 this 的指向问题。最近,朋友Z 向我求助说,他一看到 this 的题目就犯难,搞不清楚 this 究竟指向了谁。我为他作了解答,并整理成了这篇文章,但愿能帮到有须要的同窗。html
朋友Z 给我看了这样一道题:前端
var length = 10; function fn () { console.log(this.length); } var obj = { length: 5, method: function (fn) { fn(); arguments[0](); } }; obj.method(fn, 1);
问:浏览器的输出结果是什么?java
它的答案是:先输出一个 10
,而后输出一个 2
。git
让咱们来解析一下缘由:github
window
,因此输出了 10。arguments[0]();
这条语句并不常见,可能你们有疑惑的点在这里。 其实,arguments 是一种特殊的对象。在函数中,咱们无需指出参数名,就能访问。能够认为它是一种,隐式的传参形式。再来,很多同窗对 this 的指向感到疑惑,是由于 this 并无指向咱们预期的那个对象。面试
就像这道题,从语义上来看,咱们指望 fn() 输出的是 obj 本身的 length,也就是 5,而不是 10。那么若是要获得 5 的结果,咱们该如何修改这段代码呢?segmentfault
其实只要多作一步处理就好。就是让 this 指向 obj 本身。这里,咱们能够用 call 来改变 this 的指向,像下面这样:浏览器
var length = 10; function fn () { console.log(this.length); } var obj = { length: 5, method: function (fn) { // 在这里用call 将 this 指向 obj 本身 fn.call(this); } }; obj.method(fn);
输出的结果就是 5 了,搞定。数据结构
看吧,this 也没那么复杂吧,咱们只须要一些简单的操做,就能控制 this 的指向了。那么,问题来了,为何有时候 this 会失控呢?
其实,这与 this 机制背后的原理有关。不过别急,让咱们从理解 this 的基本概念开始,先来看看 this 究竟是什么?
this 是 JavaScript 中的一个关键字。它一般被运用于函数体内,依赖于函数调用的上下文条件,与函数被调用的方式有关。它指向谁,则彻底是由函数被调用的调用点来决定的。
因此,this,是在运行时绑定的,而与编写时的绑定无关。随着函数使用场合的不一样,this 的值也会发生变化。可是有一个总的原则:那就是this 总会指向,调用函数的那个对象。
从概念上理解起来,彷佛有点费劲。那咱们为何还要使用 this 呢?用了 this 会带来什么好处?
让咱们先看下面这个例子:
function identify() { return this.name.toUpperCase(); } var me = { name: "Kyle" }; var you = { name: "Reader" }; identify.call( me ); // KYLE identify.call( you ); // READER
一开始咱们可能太不明白为什么这样输出。那不如先换个思路,与使用 this 相反,咱们能够明确地将环境对象,传递给 identify()。像这样:
function identify(context) { return context.name.toUpperCase(); } identify( you ); // READER
在这个简单的例子中,结果是同样的。咱们能够把环境对象直接传入函数,这样看来比较直观。可是,当模式愈加复杂时,将执行环境做为一个明确的参数传递给函数,就会显得很是混乱了。
而 this 机制,能够提供一种更优雅的方式,来隐含地“传递”一个对象的引用,这会使得 API 的设计更加地干净,复用也会变得容易。
明白了 this 的概念以后,不经让我好奇,为什么 this 指向的就是函数运的执行环境呢?
以前,看到了 阮老师 的一篇文章,十分透彻地分析了 this 的原理。我根据本身的理解,整理以下。
不少教科书会告诉你,this 指的是函数运行时所在的环境。可是,为何会这样?也就是说,函数的运行环境究竟是怎么决定的?
理解 this 的原理,有助于帮咱们更好地理解它的用法。JavaScript 语言之因此有 this 的设计,跟内存里面的数据结构有关系。
来看一个简单的示例:
var obj = { foo: 5 };
上面的代码将一个对象赋值给变量 obj。JavaScript 引擎会先在内存里面,生成一个对象 { foo: 5 },而后把这个对象的内存地址赋值给变量 obj。
也就是说,变量 obj 其实只是一个地址。后面若是要读取 obj.foo,引擎先从 obj 拿到内存地址,而后再从该地址读出原始的对象,返回它的 foo 属性。
这样的结构很清晰,但若是属性的值是一个函数,又会怎么样呢?好比这样:
var obj = { foo: function () {} };
这时,JavaScript 引擎会将函数单独保存在内存中,而后再将函数的地址赋值给 foo 属性的 value 属性。
能够看到,函数是一个单独的值(以地址形式赋值),因此才能够在不一样的环境中执行。
又由于,JavaScript 容许在函数体内部,引用当前环境的其余变量。因此须要有一种机制,可以在函数体内部得到当前的运行环境(context)。因此,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
在理解了 this 的原理以后,咱们用下面的 5 种状况,来讨论 this 的用法。
这是函数的最一般用法,属于全局性调用,所以 this 就表明全局对象 window。
function test(){ this.x = 1; console.log(this.x); } test(); // 1
函数做为某个对象的方法调用,这时 this 就指这个上级对象。
function test(){ console.log(this.x); } var o = {}; o.x = 1; o.m = test; o.m(); // 1
所谓构造函数,就是经过这个函数生成一个新对象(object)。这时,this 就指这个新对象。
function test(){ this.x = 1; } var o = new test(); console.log(o.x); // 1
apply() 是函数对象的一个方法,它的做用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象。所以,this 指的就是这第一个参数。
var x = 0; function test() { console.log(this.x); } var o = {}; o.x = 1; o.m = test; o.m.apply(); //0
apply() 的参数为空时,默认调用全局对象。所以,这时的运行结果为0,证实this指的是全局对象。
它与上文中提到的 call 的做用是同样的,只是写法上略有区别。因为篇幅缘由,我会另启一篇,来详述它们的用法。
ES6 中的箭头函数,在大部分状况下,使得 this 的指向,变得符合咱们的预期。但有些时候,它也不是万能的,一不当心的话,this 一样会失控。
由于篇幅内容较多,我会另写一篇文章来介绍。
最后,让咱们来巩固一下 this 的概念和用法。来看一道面试题:
window.val = 1; var obj = { val: 2, dbl: function () { this.val *= 2; val *= 2; console.log('val:', val); console.log('this.val:', this.val); } }; // 说出下面的输出结果 obj.dbl(); var func = obj.dbl; func();
答案是输出:2 、 4 、 8 、 8
。
解析:
this 指代了函数当前的运行环境,依赖于函数调用的上下文条件,在运行时才会进行绑定。请牢记总原则:this 总会指向,调用函数的那个对象。
PS:欢迎关注个人公众号 “超哥前端小栈”,交流更多的想法与技术。