js 从两道面试题加深理解闭包与箭头函数中的this
壹 ❀ 引
在本文之前我已经花了两个篇幅专门介绍了JavaScript中的闭包与this,正好今早地铁上看到了两道面试题,试着做了下发现挺有意思,所以想单独写一篇文章来记录解析过程。若你对于闭包与this有所了解,不妨先看自己的理解是否正确,若你对于这部分知识欠缺,还是建议先阅读我前面的两篇文章,链接在下:
一篇文章看懂JS闭包,都要2020年了,你怎么能还不懂闭包?
js 五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解
那么本文开始。
贰 ❀ 题一
/*非严格模式*/ var name = 'window' var obj1 = { name: '听风是风', fn1: function () { console.log(this.name) }, fn2: () => console.log(this.name), fn3: function () { return function () { console.log(this.name) } }, fn4: function () { return () => console.log(this.name) } } var obj2 = { name: '行星飞行' }; obj1.fn1();//? obj1.fn1.call(obj2);//? obj1.fn2();//? obj1.fn2.call(obj2);//? obj1.fn3()();//? obj1.fn3().call(obj2);//? obj1.fn3.call(obj2)();//? obj1.fn4()();//? obj1.fn4().call(obj2);//? obj1.fn4.call(obj2)();//?
答案就不统一贴了,大家可以自己输出,这里直接开始解析:
第一个输出听风是风,fn1调用前有一个obj1,this为隐式绑定指向obj1,因此读取到obj1的name属性。
第二个输出行星飞行,在介绍this的文章中已经提到,显式绑定优先级高于隐式绑定,所以此时的this指向obj2,读取了obj2的name属性。
第三个输出window,在介绍this一文中我们已经知道箭头函数并没有自己的this,它的this指向由上层执行上下文中的this决定,那为什么上层执行上下文是window呢?
我在介绍JavaScript执行上下文的文章中已经提到,JavaScript中的上下文分为全局执行上下文,函数执行上下文与eval执行上下文(eval不作考虑)。而不管是全局上下文或函数上下文的创建,大致都包含了确认this指向,创建词法环境,创建变量环境三步。
也就是说,this属于上下文中的一部分,很明显对象obj1并不是一个函数,它并没有权利创建自己的上下文,所以没有自己的this,那么它的外层是谁呢?当然是全局window啦,所以这里的this指向window。
第四个输出window,在this介绍一文中已经提到,箭头函数的this由外部环境决定,且一旦绑定无法通过call,apply或者bind再次改变箭头函数的this,所以这里虽然使用了call方法但依旧无法修改,所以this还是指向window。
第五个输出window,这个在闭包一文中已经提到了这个例子,obj1.fn3()()其实可以改写成这样:
var fn = obj1.fn3(); fn();
先执行了fn3方法,返回了一个闭包fn,而fn执行时本质上等同于window.fn(),属于this默认绑定,所以this指向全局对象。
第六个输出行星飞行,同样是先执行fn3返回一个闭包,但闭包执行时使用了call方法修改了this,此时指向obj2,这行代码等同于:
var fn = obj1.fn3(); fn.call(obj2);//显式绑定
第七个输出window,obj1.fn3.call(obj2)()修改一下其实是这样,fn被调用时本质上还是被window调用:
var fn = obj1.fn3.call(obj2); window.fn();//默认绑定
第八个输出听风是风,fn4同样是返回一个闭包,只是这个闭包是一个箭头函数,所以箭头函数的this参考fn4的this即可,很明显此次调用fn4的this指向obj1。
var fn = obj1.fn4(); window.fn();//无法改变箭头函数this
第九个输出听风是风,改写代码其实是这样,显式绑定依旧无法改变箭头函数this:
var fn = obj1.fn4(); fn.call(obj2);//显式绑定依旧无法改变this
第十个输出行星飞行,前文已经说了,虽然无法直接改变箭头函数的this,但可以通过修改上层上下文的this达到间接修改箭头函数this的目的:
var fn = obj1.fn4.call(obj2);//fn4的this此时指向obj2 window.fn();//隐式绑定无法改变箭头函数this,this与fn4一样
OK,题目一解析完毕,我们接着看题目二,其实没有太大区别,只是两个对象是以构造函数创建罢了。
叁 ❀ 题二
/*非严格模式*/ var name = 'window' function Person(name) { this.name = name; this.fn1 = function () { console.log(this.name); }; this.fn2 = () => console.log(this.name); this.fn3 = function () { return function () { console.log(this.name) }; }; this.fn4 = function () { return () => console.log(this.name); }; }; var obj1 = new Person('听风是风'); console.dir(obj1); var obj2 = new Person('行星飞行'); obj1.fn1(); obj1.fn1.call(obj2); obj1.fn2(); obj1.fn2.call(obj2); obj1.fn3()(); obj1.fn3().call(obj2); obj1.fn3.call(obj2)(); obj1.fn4()(); obj1.fn4().call(obj2); obj1.fn4.call(obj2)();
我们开始解析第二题:
第一个输出听风是风,与第一题一样,这里同样是隐式绑定,this指向new出来的对象obj1。
第二个输出行星飞行,显式绑定,this指向obj2。
第三个你是不是觉得是window,很遗憾,这里的箭头函数指向了obj1,输出听风是风。
哎?不对啊,第一题同样是访问对象中的箭头函数,由于对象没有上下文,所以指向全局window,怎么到这里就不是全局了,new 出来的obj1与我们直接创建的对象有何区别?这就得从new一个函数发生了什么与闭包概念说起,我们先来看个简单的例子1:
function Fn(){ var name = '听风是风'; this.sayName = function () { console.log(name); }; }; var obj = new Fn(); obj.sayName();//?
请问obj.sayName能否访问到构造函数中的name属性?答案是能,这里的sayName方法其实就是一个闭包,它访问了外层函数Fn中的自由变量name,并在new过程中由构造函数Fn返回,我们可以尝试打印obj并查看sayName方法:
可以看到在scopes字段中保存了一个closure闭包,因为它的存在,返回的闭包obj.sayName才能继续访问此变量。
而我们知道new一个构造函数时,其实可以理解为就是新建了一个对象,并将构造器属性以及构造函数原型都赋予给了此对象,并最终返回,我们简单模拟其实是这样,例子2:
function Fn(){ var name = '听风是风'; var obj = {}; obj.sayName = function () { console.log(name); }; return obj; }; var obj = Fn();
同样是打印返回的obj查看sayName方法,可以看到也存在闭包:
那我们回顾到上面的箭头函数,是不是用闭包就能解释通,返回的箭头函数同样保存了构造函数的上下文,而箭头函数的this指向由上层上下文中的this决定,构造函数在new的过程中this指向了obj1,于是箭头函数的this同样也指向了obj1。
让我们回顾一遍什么是闭包?闭包是使用了外层作用域自由变量的函数,很遗憾,JavaScript似乎并未将构造器属性归为自由变量,所以这里并不能用闭包解释,看这个例子3:
function Fn(){ this.name = '听风是风'; this.sayName = function () { console.log(this.name); }; }; var obj = new Fn(); console.log(obj);
我们打印obj对象并查看sayName方法,可以看到并不是一个闭包:
不知道大家有没有理解我想表达的观点,在上面展示的例子1例子2中,返回的函数如果是访问name这样的变量就构成了闭包,但例子3中访问this.name这类构造器属性却不构成闭包。
即便如此,我们通过前面三个小例子已经证明了new操作返回的对象有权访问构造函数内部作用域,同理,对象中的箭头函数一样可访问,这种关系类似于闭包却又不是闭包,希望大家多多体会。(若大家无法理解这一段,可以看下评论区的解释)
花了比较大的篇幅解释第三个,第三个说清楚了后面的都好展开了。
那么第四个输出听风是风,我们改写代码其实是这样:
var arrowFn = obj1.fn2;//箭头函数this指向obj1 arrowFn.call(obj2);//箭头函数this无法直接改变
第五个输出window,与题一相同,返回闭包本质上被window调用,this被修改。
第六个输出行星飞行,返回闭包后利用call方法显式绑定指向obj2。
第七个输出window,返回闭包还是被window调用。
第八个输出听风是风,返回闭包是箭头函数,this同样会指向obj1,虽然返回后也是window调用,但箭头函数无法被直接修改,还是指向obj1。
第九个输出听风是风,箭头函数无法被直接修改。
第十个输出行星飞行,箭头函数可通过修改外层作用域this指向从而达到间接修改的目的。
肆 ❀ 总
那么到这里两道题分析完毕,我们来做个总结。
题一与题二虽然都是对象,但通过new创建出来的对象与对象直接量还是有所区别,这一点就体现在了对象中的箭头函数中。相比普通对象,new操作符的对象保存了构造函数上下文中的this指向,导致箭头函数并不会指向window。
箭头函数相比普通函数,箭头函数的this比较吃软饭,外层上下文中的this指向谁它便指向谁,同时我们无法直接修改箭头函数的this。而普通函数的this可以被隐式,显式多种手段修改,并满足一定优先级。
我们了解到构造函数得到的实例对象所包含的函数严格意义上并不是闭包,虽然它与闭包非常相似。
如果大家对于解析有所疑问,欢迎留言,我会第一时间回复,那么本文就写到这里。
最后补一个,如果大家对于new一个函数的过程有疑虑,建议阅读博主这篇文章 js new一个对象的过程,实现一个简单的new方法