闭包是一个老生常谈的问题,简单概括下闭包的形成的两个条件:

    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的垃圾回收机智是引用计数,当变量在其他作用域被引用时,该变量就不会被销毁,会一直存在内存中,这样就形成了闭包。在调试代码的过程中也可以在开发者工具中看出,如下图所示:

浅析js闭包-LMLPHP

  以上可以看出,当前环境存在一个作用域,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

  执行结果:浅析js闭包-LMLPHP

  再说一个经典的面试题:

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。

浅析js闭包-LMLPHP

  如何让其一次性输入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值传递给闭包,其有自己单独的作用域,不再是引用同一变量,具体作用域情况如下所示:

浅析js闭包-LMLPHP

  以上仅为个人看法,如有错误,烦请指出,谢谢!

03-11 01:02