以前写了篇《闭包初窥》,谈了一些我对闭包的浅显认识,在前文基础上,补充而且更新些对于闭包的认识。html
仍是以前的那个经典的例子,来补充些经典的解释。浏览器
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的垃圾收集器才能释放相应的内存空间。闭包
让咱们说的更透彻一些。所谓“闭包”,就是在构造函数体内定义另外的函数做为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量。这使得只要目标对象在生存期内始终能保持其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用已经结束,临时变量的名称也都消失了,但在目标对象的方法内却始终能引用到该变量的值,并且该值只能通这种方法来访问。即便再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新的值,和上次那次调用的是各自独立的。函数
仍是前文的例子:spa
<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的值。code
接下来讲说垃圾回收机制(Garbage Collecation)。htm
在上面的第一个例子中,变量始终保存在内存中,说到底与JavaScript的垃圾回收机制有关。JavaScript垃圾回收的机制很简单:找出再也不使用的变量,而后释放掉其占用的内存,可是这个过程不是实时的,由于其开销比较大,因此垃圾回收器会按照固定的时间间隔周期性的执行。再也不使用的变量也就是生命周期结束的变量,固然只多是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程当中存在,而在这个过程当中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,而后在函数中使用这些变量,直至函数结束,而闭包中因为内部函数的缘由,外部函数并不能算是结束。对象
仍是上代码说明吧:blog
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意味着切断变量与它此前引用的值之间的链接。当垃圾回收器下次运行时,就会删除这些值并回收它们占用的内存。