闭包简单的说就是一个函数有权访问另一个函数作用域中的变量。闭包常见的一种表现形式就是嵌套函数:
1 <script> 2 function create(val){ 3 return function(obj1,obj2){ 4 var value1 = obj1[val]; 5 var value2 = obj2[val]; 6 if(value1 == value2){ 7 return 0; 8 } 9 }; 10 } 11 var compare = create("name"); 12 var result = compare({name:"aaa"},{name:"bbb"}); 13 </script>
在上面的代码中,value1和value2变量可以访问外部函数中的变量val,之所以可以访问,是由于内部函数的作用域链中包含了外部函数的作用域,具体说是将外部函数的活动对象(Action Object)加入到内部函数的作用域链中。在内部匿名函数从create()中被return后,它的作用域链中就被初始化为包含create()函数的活动对象和全局变量对象,即可以任意访问外部函数中的变量;同时重要的一点,也是和非闭包函数不同的一点,在create()执行完毕后,其活动对象是不会被销毁的,当然create()的执行环境会不存在,因为匿名函数的作用域链中仍然包含着create()的活动对象,所以它还是会占用内存;除非直接销毁掉匿名函数(compare = null),后边会具体用一个实例来说明。从下图可以清晰地说明以上代码的外部函数和内部匿名函数的作用域链结构:
1.闭包与变量
作用域链的这种配置机制有一个副作用,即闭包只能取得包含函数中任何变量的最后一个值,因为闭包所保存的是整个变量对象,而不是某个特殊的变量。
1 <script> 2 function close(){ 3 var result = []; 4 for(var i = 0;i < 10;i++){ 5 result[i] = function(){ 6 return i; 7 }; 8 } 9 return result; 10 } 11 </script>
以上代码返回一个函数数组,直观地可能会认为,result数组的值就是从0到9,但是结果却都是10,因为每个函数的作用域链中都保存着close()函数的活动对象,所以它们都引用同样的i——10;但是,可以通过创建另一个匿名函数强制让闭包符合我们想要的结果:
1 <script> 2 function close(){ 3 var result = []; 4 for(var i = 0;i < 10;i++){ 5 result[i] = function(num){ 6 return function(){ 7 return num; 8 }; 9 }(i); 10 } 11 return result; 12 } 13 </script>
这次,我们并非是直接将闭包赋值给result,而是通过匿名函数的方式,将立即执行该匿名函数的返回结果赋值给result数组,参数num接受传入的变量i,在这个匿名函数中又创建一个闭包,来实现值传递。
2.闭包this对象
在闭包中使用this对象会导致一些问题的发生。在全局函数中,this指代window;在函数的内部,this指代当前对象。还有,匿名函数的this具有全局性,通常指向window;不过由于闭包的不同利用方式,出现了不同的情况。
1 <script> 2 var gg = "global"; 3 var obj = { 4 gg:"local", 5 func:function(){ 6 return function(){ 7 return this.gg; 8 }; 9 } 10 }; 11 console.log(obj.func()());//global 12 </script>
以上为匿名函数取得其作用域外的this对象,原因就是内部函数在搜索this时,仅仅搜索到自己的活动对象为止,不会去搜索外部函数的活动对象。不过,可以利用将this对象赋值给局部变量的方法可以实现访问对象内部变量,以下就是利用了一个that局部变量轻松实现访问对象内属性的方法。
1 <script> 2 var gg = "global"; 3 var obj = { 4 gg:"local", 5 func:function(){ 6 var that = this; 7 return function(){ 8 return that.gg; 9 }; 10 } 11 }; 12 console.log(obj.func()());//local 13 </script>
3.内存泄漏
在IE9之前的版本中对JScript对象和COM(Component Object Model)对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些问题。例如,如果闭包的作用域链中存在一个HTML元素,那么意味着该元素无法销毁。
1 <script> 2 function func(){ 3 var ele = document.getElementById("divs"); 4 ele.onclick = function(){ 5 alert(ele.id); 6 } 7 } 8 </script>
以上代码功能通过一个闭包来创建一个点击事件,由于匿名函数中保存了func()的活动对象,所以无法减少ele的引用数且至少为1,内存永远不会回收;不过可以通过局部变量方式处理:
1 <script> 2 function func(){ 3 var ele = document.getElementById("divs"); 4 var eleid = ele.id;//将要获取的内容保存在局部变量中,使得匿名函数中释放了对HTML元素的循环引用 5 ele.onclick = function(){ 6 alert(eleid); 7 } 8 ele = null;//将ele设置为null,可以解除对DOM对象的引用,回收内存 9 } 10 </script>
将ele设置为null是必要的,因为在函数的活动对象中保留着一个引用,设置为null即可解除。