很久前的一个问题终于得以解决,内心是无比喜悦的,不多说,先上代码:
1 function test(){ 2 for(var i=0;i<5;i++){ 3 window.onclick=function(){ 4 alert(i); 5 } 6 } 7 }
8 test()
原意是点击第一下弹出i的值0,点击第二下弹出i的值1,一直到第五次点击弹出4。想象是美好的,现实却不按常规出牌,无论点击多少次,alert出来的值都是5。后来接触了js闭包,才知道原来是它搞的鬼。看了不少相关书籍和博客后,发现其实它也没有想象得那么难。
我理解的闭包是可以访其他函数作用域的函数,在js中,通常通过在一个函数内部定义另一个函数来实现,如上例。提到闭包,应首先理解JavaScript特有的链式作用域,即一个函数作用域链的第一位是该函数本身的活动对象,第二位是包含该函数(即父函数)活动对象的索引,第三位是父函数的父函数活动对象的索引,以此类推,直至全局变量对象。注意,反之不成立,即父函数的作用域不包含子函数变量对象的索引。如图:
在文章开头的例子中,每个匿名函数(循环了五次,相当于五个匿名函数)的作用域链中都保存着test的活动对象,所以它们引用的是同一个变量i。当函数返回后,i的值是5,此时,每个函数都引用着保存变量i的对象,所以在每个函数内部,i的值都是5.
闭包具有两面性,有缺点,也有优点,在上例中因为对闭包使用不当没有达到想要的效果,但在一些特殊的应用中,只有用闭包才能实现相应的功能,接下来,就谈谈闭包的用途(优点)和注意事项(缺点)。
1、用途
- 从外部读取某个函数的局部变量。
子函数可以访问父函数的作用域,那么如果全局作用域需要访问函数的局部变量怎么办?
function myA(){ var a=6; function myB(){ alert(a); } return myB; } var result=myA(); result();
在上述代码中,无法直接访问myA内的变量a,但是可以再内部定义一个函数myB,该函数可以访问它的父函数myA的作用域内的变量a,然后将这个函数返回并赋值给result,相当于在全局作用域内调用myB。
- 让某个局部变量始终保存在内存中
js的内存回收机制是这样的:当调用一个函数时,为它的局部变量对象分配相应的内存空间,一旦调用结束,则收回为它分配的内存。但是这样会有一个bug,即若在函数A内定义了函数B,而B也调用了A内的局部变量x,且B随时可能被全局调用,因为A调用完毕后内存被收回,所以B访问不到x。js的解决办法是,在定义一个函数时,把这个函数和它可能使用到的变量(包括自身变量和父函数的变量),统统一起保存起来,构成一个闭包,即便当父函数调用完毕,这些变量占用的内存也不会被收回。只有当该函数不可能被调用时,才会收回这些内存。
若需要随时访问某个局部变量,则可以通过该方法,构建一个闭包,让一个局部变量始终保存在内存中。
2、缺点
- 占用过多的内存
上述js的回收机制说明,只要闭包不被销毁,它的作用域链所占用的内存便不能被释放,若过多地、无用地占用内存,对效率和性能极为不利。
- 内存泄露
由于IE9之前的版本对JavaScript和COM对象使用不同的垃圾回收机制(因为IE9之前中的部分对象不是原生JavaScript对象,如其中的BOM和DOM就是以c++的COM对象实现的,所以回收机制不同),所以只要在IE9之前版本中涉及COM对象,就会导致循环引用,以至于对象内存不能被收回。具体来说就是,如果闭包的作用域中保存着一个HTML元素,则意味着该元素将无法被销毁,例如:
function f1(){ var e=document.getElementById("top"); element.onclick=function(){ alert(e.id); }; }
上述代码为e元素事件创建了一个闭包,即匿名函数,由于匿名函数包含着对f1活动对象的引用,只要匿名函数存在,e元素的引用次数至少为1,因此它占用的内存便永远不会被收回(IE9对COM对象的垃圾回收机制决定的)。该问题也有相应的解决方法,请读者自行思考。
最后,关于闭包中this对象,需注意,由于匿名函数的作用域具有全局性,所以慎用闭包的this对象。没有用var声明的局部变量也具有全局性。