JS闭包
1.什么是闭包?
维基百科解释为:
在计算机科学中,闭包(Closure)是词法闭包(Lexcial Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造他的环境也不例外。所以,另有一种说法认为闭包是自由函数和与其相关的引用环境组合而成的实体。简而言之,闭包就是一个函数和他用到的变量组成的对象。
1 <body> 2 <script type="text/javascript"> 3 function f1() { 4 var x = 10; 5 var y = 20; 6 return function f2() { 7 console.log(x); 8 } 9 } 10 11 var result = f1(); 12 result(); 13 </script> 14 </body>
上面代码在运行的时候,浏览器显示如下:
由函数 f2 捕获的变量x在函数外面使用的时候依然存在,即函数 f2 和变量x的这个组合便是闭包。
2.闭包是什么时候创建的?
在上面代码中,在预处理阶段,函数 f1 被扫描,创建 f1 的词法环境,然后把 f1 里面的 x, y 添加到 f1 的词法环境中,
然后 f2.[[scope]]被创建,并且 f2.[[scope]] == f1.le,
然后扫描 f2 ,发现其用到父函数里面的变量,于是将用到的变量放到一个闭包对象Closure 里面去。
也就是说,在函数 f2 被创建的时候,闭包已经存在。当然,不同的浏览器对闭包对象的处理略有不同。
在JS里面,只有当一个函数捕获其上级函数的变量时才会形成闭包。
还有一点,如下代码所示:
1 <body> 2 <script type="text/javascript"> 3 function f1() { 4 var m = 10; 5 function f2() { 6 var n = 20; 7 function f3() { 8 console.log(m); 9 } 10 f3(); 11 } 12 f2(); 13 } 14 f1(); 15 </script> 16 </body>
执行结果为:
即内部函数不一定要返回到外部去被调用才会形成闭包,而是只要被调用就会创建Closure。
并且由调用栈(Call Stack)可以看出 ,函数 f2 被创建的时候,闭包对象被创建,接着函数 f3 被创建,f3 被调用,f2 被调用,然后闭包对象销毁。
所以说,闭包的本质就是词法环境构成的作用域链。
3.闭包的好处
3.1减少全局变量
假设有一个重复调用达到累加效果的函数,一般来讲应该是这样的
1 <body> 2 <script type="text/javascript"> 3 var a = 0; 4 function f() { 5 a++; 6 alert(a); 7 } 8 f(); 9 f(); 10 f(); 11 </script> 12 </body>
首先定义一个全局变量,然后对其进行类加,但是,使用闭包之后,神奇的事情发生了:
1 <body> 2 <script type="text/javascript"> 3 function f() { 4 var a = 0; 5 return function f1() { 6 a++; 7 alert(a) 8 } 9 } 10 var result = f(); //f()的返回值是一个函数f1() 11 result(); //所以这里调用result()就等于调用绑定了变量a的f1() 12 result(); 13 result(); 14 </script> 15 </body>
没有创建全局变量,依然达到了相同的效果。
该方法将函数f的成员变量a与f1组合为一个闭包对象f,然后将f赋给result,此时在result上a与f1是被绑定的,所以每次调用result,都相当于给redult.a + 1;
3.2减少传递给函数的参数数量
1 <body> 2 <script type="text/javascript"> 3 //创建一个工厂方法,接收一个base参数,返回一个基于这个参数的计算方法 4 function callFactory(base) { 5 //计算方法,接收一个参数,用于计算范围 6 return function(max) { 7 var total = 0; 8 for (let i = 0; i < max; i++) { 9 total += i; 10 } 11 //计算方法的返回值 12 return total + base; 13 } 14 } 15 var adder = callFactory(2);//这里调用callFactory(2)工厂方法返回了一个基于参数2的function(max) 16 alert(adder(3));//这里调用adder(3)就相当于调用基于参数2的function(3) 17 alert(adder(4)); 18 </script> 19 </body>
在这个函数中,父函数返回子函数,父函数的形参base和子函数组成了闭包,所以只需要传递一次base参数,就将base绑定到了子函数上,后面就不用再传递base参数,而只需要传递子函数的参数即可。
当给外层函数callFactory()传递不同的参数时,形成不同的闭包对象,真是神奇的写法。
3.3封装
例如:使用闭包实现get/set方法的简单封装
1 <body> 2 <script type="text/javascript"> 3 (function() { 4 var m = 0; 5 function getM() { 6 return m; 7 } 8 function setM(param) { 9 m = param; 10 } 11 window.g = getM; //由于在浏览器中执行,这里先挂载在window对象上 12 window.s = setM; 13 })(); 14 s(12); 15 alert(g()); 16 </script> 17 </body>
4.使用闭包的注意点
- 捕获的变量只是个引用,而不是复制。
- 父函数每调用一次,都会产生一个新的闭包,因为函数每次调用都会创建新的词法环境。
- 使用闭包解决循环中的回调函数的异步问题。
有如下代码,给div元素添加点击事件,使之弹出不同的值:
1 <body> 2 <div id="1">1</div> 3 <div id="2">2</div> 4 <div id="3">3</div> 5 <script type="text/javascript"> 6 for (var i = 1; i <= 3; i++) { 7 var di = document.getElementById(i); 8 di.onclick = function () { 9 alert(i); 10 } 11 } 12 </script> 13 </body>
由于for循环中用var声明的变量相当于全局变量,所以在异步的回调函数中执行的时候总是弹出循环的结尾4,而不是需要的1,2,3;
这种情况最简单的解决办法就是使用let而不是var声明变量就OK了,但是在这里还是要用一下闭包,只是为了加深理解。
1 <body> 2 <div id="1">1</div> 3 <div id="2">2</div> 4 <div id="3">3</div> 5 <script type="text/javascript"> 6 for (var i = 1; i <= 3; i++) { 7 var di = document.getElementById(i); 8 di.onclick = (function (id) { //每次循环时,通过一个立即调用的函数,将i与函数绑定形成不同的闭包 9 return function() { 10 alert(id); 11 } 12 })(i); 13 } 14 </script> 15 </body>
当然还可以用this对象绑定等达到相同效果。