由工作中演变而来的面试题
这是一个我工作当中的遇到的一个问题, 似乎很有趣, 就当做了一道题去面试, 发现几乎没人能全部答对并说出原因, 遂拿出来聊一聊吧
先看题目代码:
- 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;
- }
遂用上述函数测试是否为匿名函数:
可以得知变量 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 (){} }, 也属于函数表达式
4Function 构造函数
可以给 Function 构造函数传一个函数字符串, 返回包含这个字符串命令的函数, 此种方法创建的是匿名函数
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 报错
测试 2, 非对象内部的函数表达式:
- var fn=function (){
- console.log(fn);
- };
- fn();//function (){console.log(fn);}; 正确
结论是: 使用 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 的闭包有了更深入的了解
关于什么是闭包, 网上的文章数不胜数, 但理解什么是闭包还是要在代码中自己去发现与领悟
如果要我说什么是闭包, 我认为, 广义上的闭包就是指一个变量在他自身作用域外被使用了, 就叫发生了闭包
希望读者能通过本文对闭包现象有进一步的了解, 如有其它见解或看法, 欢迎指正或留言讨论(完)
来源: http://www.codeceo.com/article/javascript-closure-interview.html