一不小心就做错的JS闭包面试题


Posted in Javascript onNovember 25, 2015

由工作中演变而来的面试题

这是一个我工作当中的遇到的一个问题,似乎很有趣,就当做了一道题去面试,发现几乎没人能全部答对并说出原因,遂拿出来聊一聊吧。

先看题目代码:

function fun(n,o) {
 console.log(o)
 return {
  fun:function(m){
   return fun(m,n);
  }
 };
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,?
//问:三行a,b,c的输出分别是什么?

这是一道非常典型的JS闭包问题。其中嵌套了三层fun函数,搞清楚每层fun的函数是那个fun函数尤为重要。

可以先在纸上或其他地方写下你认为的结果,然后展开看看正确答案是什么?

答案

//a: undefined,0,0,0
//b: undefined,0,1,2
//c: undefined,0,1,1

都答对了么?如果都答对了恭喜你在js闭包问题当中几乎没什么可以难住你了;如果没有答案,继续往下分析。

JS中有几种函数

首先,在此之前需要了解的是,在JS中函数可以分为两种,具名函数(命名函数)和匿名函数。

区分这两种函数的方法非常简单,可以通过输出 fn.name 来判断,有name的就是具名函数,没有name的就是匿名函数

注意:在低版本IE上无法获取具名函数的name,会返回undefined,建议在火狐或是谷歌浏览器上测试

或是采用兼容IE的获取函数name方法来获取函数名称:

/**
  * 获取指定函数的函数名称(用于兼容IE)
  * @param {Function} fun 任意函数
  */
function getFunctionName(fun) {
  if (fun.name !== undefined)
    return fun.name;
  var ret = fun.toString();
  ret = ret.substr('function '.length);
  ret = ret.substr(0, ret.indexOf('('));
  return ret;
}

遂用上述函数测试是否为匿名函数:

一不小心就做错的JS闭包面试题

可以得知变量fn1是具名函数,fn2是匿名函数

创建函数的几种方式

说完函数的类型,还需要了解JS中创建函数都有几种创建方法。

1、声明函数

最普通最标准的声明函数方法,包括函数名及函数体。

function fn1(){} 

2、创建匿名函数表达式

创建一个变量,这个变量的内容为一个函数

var fn1=function (){}
注意采用这种方法创建的函数为匿名函数,即没有函数name

var fn1=function (){};
getFunctionName(fn1).length;//0

3、创建具名函数表达式

创建一个变量,内容为一个带有名称的函数

var fn1=function xxcanghai(){};
注意:具名函数表达式的函数名只能在创建函数内部使用

即采用此种方法创建的函数在函数外层只能使用fn1不能使用xxcanghai的函数名。xxcanghai的命名只能在创建的函数内部使用

测试:

var fn1=function xxcanghai(){
  console.log("in:fn1<",typeof fn1,">xxcanghai:<",typeof xxcanghai,">");
};
console.log("out:fn1<",typeof fn1,">xxcanghai:<",typeof xxcanghai,">");
fn1();
//out:fn1< function >xxcanghai:< undefined >
//in:fn1< function >xxcanghai:< function >

可以看到在函数外部(out)无法使用xxcanghai的函数名,为undefined。

注意:在对象内定义函数如var o={ fn : function (){…} },也属于函数表达式 

4、Function构造函数

可以给 Function 构造函数传一个函数字符串,返回包含这个字符串命令的函数,此种方法创建的是匿名函数。

一不小心就做错的JS闭包面试题

5、自执行函数

(function(){alert(1);})();
(function fn1(){alert(1);})();

自执行函数属于上述的“函数表达式”,规则相同

 6、其他创建函数的方法

当然还有其他创建函数或执行函数的方法,这里不再多说,比如采用 eval , setTimeout , setInterval 等非常用方法,这里不做过多介绍,属于非标准方法,这里不做过多展开

三个fun函数的关系是什么?

说完函数类型与创建函数的方法后,就可以回归主题,看这道面试题。

这段代码中出现了三个fun函数,所以第一步先搞清楚,这三个fun函数的关系,哪个函数与哪个函数时相同的。

function fun(n,o) {
 console.log(o)
 return {
  fun:function(m){
   //...
  }
 };
}

先看第一个fun函数,属于标准具名函数声明,是新创建的函数,他的返回值是一个对象字面量表达式,属于一个新的object。

这个新的对象内部包含一个也叫fun的属性,通过上述介绍可得知,属于匿名函数表达式,即fun这个属性中存放的是一个新创建匿名函数表达式。

注意:所有声明的匿名函数都是一个新函数。

所以第一个fun函数与第二个fun函数不相同,均为新创建的函数。

函数作用域链的问题

再说第三个fun函数之前需要先说下,在函数表达式内部能不能访问存放当前函数的变量。

测试1:对象内部的函数表达式:

var o={
 fn:function (){
  console.log(fn);
 }
};
o.fn();//ERROR报错

一不小心就做错的JS闭包面试题

测试2:非对象内部的函数表达式:

var fn=function (){
 console.log(fn);
};
fn();//function (){console.log(fn);};正确

一不小心就做错的JS闭包面试题

结论:使用var或是非对象内部的函数表达式内,可以访问到存放当前函数的变量;在对象内部的不能访问到。

原因也非常简单,因为函数作用域链的问题,采用var的是在外部创建了一个fn变量,函数内部当然可以在内部寻找不到fn后向上册作用域查找fn,而在创建对象内部时,因为没有在函数作用域内创建fn,所以无法访问。

 

所以综上所述,可以得知,最内层的return出去的fun函数不是第二层fun函数,是最外层的fun函数。

所以,三个fun函数的关系也理清楚了,第一个等于第三个,他们都不等于第二个。

到底在调用哪个函数?

再看下原题,现在知道了程序中有两个fun函数(第一个和第三个相同),遂接下来的问题是搞清楚,运行时他执行的是哪个fun函数?

function fun(n,o) {
 console.log(o)
 return {
  fun:function(m){
   return fun(m,n);
  }
 };
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,?
//问:三行a,b,c的输出分别是什么?

1、第一行a

var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);
可以得知,第一个fun(0)是在调用第一层fun函数。第二个fun(1)是在调用前一个fun的返回值的fun函数,所以:

第后面几个fun(1),fun(2),fun(3),函数都是在调用第二层fun函数。

遂:

在第一次调用fun(0)时,o为undefined;

第二次调用fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;

第三次调用fun(2)时m为2,但依然是调用a.fun,所以还是闭包了第一次调用时的n,所以内部调用第一层的fun(2,0);所以o为0

第四次同理;

即:最终答案为undefined,0,0,0

2、第二行b

var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
先从fun(0)开始看,肯定是调用的第一层fun函数;而他的返回值是一个对象,所以第二个fun(1)调用的是第二层fun函数,后面几个也是调用的第二层fun函数。

遂:

在第一次调用第一层fun(0)时,o为undefined;

第二次调用 .fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;

第三次调用 .fun(2)时m为2,此时当前的fun函数不是第一次执行的返回对象,而是第二次执行的返回对象。而在第二次执行第一层fun函数时时(1,0)所以n=1,o=0,返回时闭包了第二次的n,遂在第三次调用第三层fun函数时m=2,n=1,即调用第一层fun函数fun(2,1),所以o为1;

第四次调用 .fun(3)时m为3,闭包了第三次调用的n,同理,最终调用第一层fun函数为fun(3,2);所以o为2;

即最终答案:undefined,0,1,2

 3、第三行c

var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?
根据前面两个例子,可以得知:

fun(0)为执行第一层fun函数,.fun(1)执行的是fun(0)返回的第二层fun函数,这里语句结束,遂c存放的是fun(1)的返回值,而不是fun(0)的返回值,所以c中闭包的也是fun(1)第二次执行的n的值。c.fun(2)执行的是fun(1)返回的第二层fun函数,c.fun(3)执行的也是fun(1)返回的第二层fun函数。

遂:

在第一次调用第一层fun(0)时,o为undefined;

第二次调用 .fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;

第三次调用 .fun(2)时m为2,此时fun闭包的是第二次调用的n=1,即m=2,n=1,并在内部调用第一层fun函数fun(2,1);所以o为1;

第四次.fun(3)时同理,但依然是调用的第二次的返回值,遂最终调用第一层fun函数fun(3,1),所以o还为1

即最终答案:undefined,0,1,1

后话

这段代码原本是在做一个将异步回调改写为同步调用的组件时的代码,发现了这个坑,对JS的闭包有了更深入的了解。

关于什么是闭包,网上的文章数不胜数,但理解什么是闭包还是要在代码中自己去发现与领悟。

如果要我说什么是闭包,我认为,广义上的闭包就是指一个变量在他自身作用域的被使用了,就叫发生了闭包。

大家都答对了吗?希望读者能通过本文对闭包现象有进一步的了解,如有其它见解或看法,欢迎指正讨论。

Javascript 相关文章推荐
JavaScript 字符编码规则
May 04 Javascript
JS关键字球状旋转效果的实例代码
Nov 29 Javascript
js实现感应鼠标图片透明度变化的方法
Feb 20 Javascript
轻松掌握JavaScript装饰者模式
Aug 27 Javascript
jQuery图片切换动画特效
Nov 02 Javascript
关于bootstrap日期转化,bootstrap-editable的简单使用,bootstrap-fileinput的使用详解
May 12 Javascript
AngularJS中使用three.js的实例详解
Jul 21 Javascript
原生js实现简单的模态框示例
Sep 08 Javascript
vue组件中使用iframe元素的示例代码
Dec 13 Javascript
Layui数据表格跳转到指定页的实现方法
Sep 05 Javascript
VScode格式化ESlint方法(最全最好用方法)
Sep 10 Javascript
vue 监听窗口变化对页面部分元素重新渲染操作
Jul 28 Javascript
javascript Slip.js实现整屏滑动的手机网页
Nov 25 #Javascript
javascript巧用eval函数组装表单输入项为json对象的方法
Nov 25 #Javascript
javascript数据结构之双链表插入排序实例详解
Nov 25 #Javascript
js获取图片宽高的方法
Nov 25 #Javascript
javascript数据结构之二叉搜索树实现方法
Nov 25 #Javascript
javascript常用经典算法实例详解
Nov 25 #Javascript
javascript实现很浪漫的气泡冒出特效
Sep 05 #Javascript
You might like
PHP获取网页标题的3种实现方法代码实例
2014/04/11 PHP
win7 64位系统 配置php最新版开发环境(php+Apache+mysql)
2014/08/15 PHP
php使用curl通过代理获取数据的实现方法
2016/05/16 PHP
简单谈谈PHP中的trait
2017/02/25 PHP
Laravel创建数据库表结构的例子
2019/10/09 PHP
jQuery 加上最后自己的验证
2009/11/04 Javascript
JS在IE和FF下attachEvent,addEventListener学习笔记
2009/11/26 Javascript
JavaScrip单线程引擎工作原理分析
2010/09/04 Javascript
javascript得到当前页的来路即前一页地址的方法
2014/02/18 Javascript
[原创]JQuery 在表单提交之前修改 提交的值
2016/04/14 Javascript
javascript 分号总结及详细介绍
2016/09/24 Javascript
JQuery ZTree使用方法详解
2017/01/07 Javascript
微信小程序 css使用技巧总结
2017/01/09 Javascript
一步步教会你微信小程序的登录鉴权
2018/04/09 Javascript
详解封装基础的angular4的request请求方法
2018/06/05 Javascript
vue实现自定义多选按钮
2020/07/16 Javascript
简单谈谈python中的多进程
2016/11/06 Python
TensorFlow在MAC环境下的安装及环境搭建
2017/11/14 Python
解决tensorflow测试模型时NotFoundError错误的问题
2018/07/27 Python
python读取Excel实例详解
2018/08/17 Python
解决PyCharm控制台输出乱码的问题
2019/01/16 Python
selenium+python截图不成功的解决方法
2019/01/30 Python
Python检测数据类型的方法总结
2019/05/20 Python
pycharm 批量修改变量名称的方法
2019/08/01 Python
django中使用POST方法获取POST数据
2019/08/20 Python
使用python+whoosh实现全文检索
2019/12/09 Python
python之pymysql模块简单应用示例代码
2019/12/16 Python
python lambda函数及三个常用的高阶函数
2020/02/05 Python
中国综合性网上购物商城:当当(网上卖书起家)
2016/11/16 全球购物
运动会开幕式邀请函
2014/02/03 职场文书
网络技术专业推荐信
2014/02/20 职场文书
文化产业实施方案
2014/06/07 职场文书
安全标语口号
2014/06/09 职场文书
机械专业求职信范文
2014/07/15 职场文书
湖南省召开党的群众路线教育实践活动总结大会报告
2014/10/21 职场文书
利用Python实现模拟登录知乎
2022/05/25 Python