闭包是一个老生常谈的问题,简单概括下闭包的形成的两个条件:
1、定义在函数内部
2、函数内部引用父层作用域变量
举一个最简单的例子:
1 function test() { 2 var a = 1; 3 4 function hello() { 5 console.log(a); 6 } 7 return hello; 8 } 9 10 var world = test(); 11 world(); //1
以上代码会在控制台输出“1”。这是什么为什么呢?函数内部变量在调用结束后一般都会销毁,以上代码在test方法调用结束后并没有被销毁,这是由于js语言本身垃圾回收导致的。众所周知,js的垃圾回收机制是引用计数,当变量在其他作用域被引用时,该变量就不会被销毁,会一直存在内存中,这样就形成了闭包。在调试代码的过程中也可以在开发者工具中看出,如下图所示:
以上可以看出,当前环境存在一个作用域,Closure(text),包含引用父层的作用域的变量a。
js函数参数传递分为数值传递和引用传递,数值传递针对参数的类型为number/string/boolean....;引用传递针对参数类型为object/array....;所以在针对引用父层参数时,要注意父层参数类型,如果为数值传递,父层参数改变并不会影响闭包执行结果,但是如果参数是引用传递,要注意闭包里的参数引用的是父层变量地址,变量发生改变,闭包的执行结果会随之改变,以下代码可以看出:
1 var obj = { 2 num: { 3 b: 1 4 } 5 } 6 7 function test(obj) { 8 var a = obj.num; 9 10 function hello() { 11 console.log(a.b); 12 } 13 return hello; 14 } 15 16 var world = test(obj); 17 world(); //1 18 obj.num.b = 2; 19 test(obj); 20 world(); //2
执行结果:
再说一个经典的面试题:
1 for (var i = 0; i < 5; i++) { 2 setTimeout(function() { 3 console.log(i); 4 }, 0); 5 }
想必大家都知道这段代码的执行结果是5个1,但是有没有想过为什么会是5个1呢?
setTimeout函数比较特殊,其执行优先级要低于for循环,循环结束后才会被执行,其匿名函数类似于一个回调,匿名函数都引用了变量i,var声明的变量又不存在块级作用域,最终循环结束后,i值变为5,在回调执行过程中,会向父层作用域执行RHS查询(就是查找变量i的值是否存在),i的值目前存在于全局变量window中,但是已变成了5,所以会输出5个5。
如何让其一次性输入0,1,2,3,4呢?
其实问题的本源在于js语言中本来不存在块级作用域,for循环声明的变量i最终是在全局变量中,每次循环都会更新一次全局变量中的值,所以实现块级作用域有两种常用的方法,一是借助es6的声明变量的方法let,第二个是利用闭包;
闭包的代码如下:
1 for (var i = 0; i < 5; i++) { 2 setTimeout((function(i) { 3 console.log(i); 4 })(i), 0) 5 }
正如文章刚开始所述,setTimeout回调中引用的是父层函数中变量i的值,在每次声明的时候,都会将当前的i值传递给闭包,其有自己单独的作用域,不再是引用同一变量,具体作用域情况如下所示:
以上仅为个人看法,如有错误,烦请指出,谢谢!