闭包拾遗 & 垃圾回收机制
闭包拾遗
之前写了篇《闭包初窥》,谈了一些我对闭包的浅显认识,在前文基础上,补充并且更新些对于闭包的认识。
还是之前的那个经典的例子,来补充些经典的解释。
function outerFn() { var a = 0; function innerFn() { console.log(a++); } return innerFn; } var fn = outerFn(); fn(); // 0 fn(); // 1
这里并没有在outerFn内部修改全局变量,而是从outerFn中返回了一个对innerFn的引用。通过调用outerFn能够获得这个引用,而且这个引用可以可以保存在变量中。 这种即使离开函数作用域的情况下仍然能够通过引用调用内部函数的事实,意味着只要存在调用内部函数的可能,JavaScript就需要保留被引用的函数。而且JavaScript运行时需要跟踪引用这个内部函数的所有变量,直到最后一个变量废弃,JavaScript的垃圾收集器才能释放相应的内存空间。
让我们说的更透彻一些。所谓“闭包”,就是在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量。这使得只要目标对象在生存期内始终能保持其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用已经结束,临时变量的名称也都消失了,但在目标对象的方法内却始终能引用到该变量的值,而且该值只能通这种方法来访问。即使再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新的值,和上次那次调用的是各自独立的。
还是前文的例子:
<ul> <li>0</li> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul> <script> var lis = document.getElementsByTagName('li'); for(var i = 0; i < lis.length; i++) { ~function(num) { lis[i].onclick = function() { alert(num) }; }(i) } </script>
为什么不加立即执行函数,alert的都会是5呢?
如果不加IIFE,当i的值为5的时候,判断条件不成立,for循环执行完毕,但是因为每个li的onclick方法这时候为内部函数,所以i被闭包引用,内存不能被销毁,i的值会一直保持5,直到程序改变它或者所有的onclick函数销毁(主动把函数赋为null或者页面卸载)时才会被回收。这样每次我们点击li的时候,onclick函数会查找i的值(作用域链是引用方式),一查等于5,然后就alert给我们了。加上IIFE后即是又创建了一层闭包,函数声明放在括号内就变成了表达式,后面再加上括号就是调用了,这时候把i当参数传入,函数立即执行,num保存每次i的值。
垃圾回收机制(GC)
接下来说说垃圾回收机制(Garbage Collecation)。
在上面的第一个例子中,变量始终保存在内存中,说到底与JavaScript的垃圾回收机制有关。JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。
还是上代码说明吧:
function fn1() { var obj = {name: 'hanzichi', age: 10}; } function fn2() { var obj = {name:'hanzichi', age: 10}; return obj; } var a = fn1(); var b = fn2();
我们来看代码是如何执行的。首先定义了两个function,分别叫做fn1和fn2,当fn1被调用时,进入fn1的环境,会开辟一块内存存放对象{name: 'hanzichi', age: 10},而当调用结束后,出了fn1的环境,那么该块内存会被js引擎中的垃圾回收器自动释放;在fn2被调用的过程中,返回的对象被全局变量b所指向,所以该块内存并不会被释放。
垃圾回收机制的种类
函数中的局部变量的生命周期:局部变量只在函数执行的过程中存在。而在这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储它们的值。然后在函数中使用这些变量,直至函数执行结束。此时,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用。在这种情况下,很容易判断变量是否还有存在的必要;但并非所有情况下都这么容易就能得出结论。垃圾回收器必须跟踪哪个变量有用,哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略。
- 标记清除
js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
到2008年为止,IE、Firefox、Opera、Chrome、Safari的js实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。
- 引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
Netscape Navigator3是最早使用引用计数策略的浏览器,但很快它就遇到一个严重的问题:循环引用。循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。
function fn() { var a = {}; var b = {}; a.pro = b; b.pro = a; } fn();
以上代码a和b的引用次数都是2,fn()执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为a和b的引用次数不为0,所以不会被垃圾回收器回收内存,如果fn函数被大量调用,就会造成内存泄露。
我们知道,IE中有一部分对象并不是原生js对象。例如,其DOM和BOM中的对象就是使用C++以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数策略。因此,即使IE的js引擎采用标记清除策略来实现,但js访问的COM对象依然是基于引用计数策略的。换句话说,只要在IE中涉及COM对象,就会存在循环引用的问题。
var element = document.getElementById("some_element"); var myObject = new Object(); myObject.e = element; element.o = myObject;
这个例子在一个DOM元素(element)与一个原生js对象(myObject)之间创建了循环引用。其中,变量myObject有一个名为element的属性指向element对象;而变量element也有一个属性名为o回指myObject。由于存在这个循环引用,即使例子中的DOM从页面中移除,它也永远不会被回收。
为了避免类似这样的循环引用问题,最好是在不使用它们的时候手工断开原生js对象与DOM元素之间的连接:
myObject.element = null; element.o = null;
将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾回收器下次运行时,就会删除这些值并回收它们占用的内存。