|
本文作者从一道经典的JS面试题,分享了自己对JS闭包的理解。
由工作中演变而来的面试题 这是一个我工作当中的遇到的一个问题,似乎很有趣,就当做了一道题去面试,发现几乎没人能全部答对并说出原因,遂拿出来聊一聊吧。
先看题目代码:
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);
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1); c.fun(2); c.fun(3); |
//问:三行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、创建匿名函数表达式
创建一个变量,这个变量的内容为一个函数
注意采用这种方法创建的函数为 匿名函数,即没有函数 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 构造函数传一个函数字符串,返回包含这个字符串命令的函数,此种方法创建的是 匿名函数。

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); |
先从 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的闭包有了更深入的了解。
关于什么是闭包,网上的文章数不胜数,但理解什么是闭包还是要在代码中自己去发现与领悟。
如果要我说什么是闭包,我认为,广义上的闭包就是指一个变量在他自身作用域外被使用了,就叫发生了闭包。
希望读者能通过本文对闭包现象有进一步的了解,如有其它见解或看法,欢迎指正或留言讨论。
----------------------------
原文链接:https://www.jianshu.com/p/7ff589e78964
程序猿的技术大观园:www.javathinker.net
[这个贴子最后由 flybird 在 2021-08-30 20:50:28 重新编辑]
|
|