闭包也许是 JS 中最有用的特性了. 有一份比较好的介绍闭包原理的文档.html
有一点须要牢记, 闭包保留了一个指向它封闭做用域的指针, 因此, 在给 DOM 元素附加闭包时, 极可能会产生循环引用, 进一步致使内存泄漏. 好比下面的代码:算法
function foo(element, a, b) { element.onclick = function() { /* uses a and b */ }; }
这里, 即便没有使用 element
, 闭包也保留了 element
, a
和 b
的引用, . 因为 element
也保留了对闭包的引用, 这就产生了循环引用, 这就不能被 GC 回收. 这种状况下, 可将代码重构为:闭包
function foo(element, a, b) { element.onclick = bar(a, b); } function bar(a, b) { return function() { /* uses a and b */ } }
————— 谷歌js编码规范
这是谷歌js编码规范里的。"闭包",是个绕不开的话题,我查阅了很多资料,各类解释都有,关于为何会形成内存泄漏,也都介绍的很晦涩,没有把原理讲透,在这里,我就这两个问题详细讲一下。
首先讲闭包。闭包简而言之,就是一个函数(fa),的内部函数(fb)被fa外的变量引用,就造成了一个闭包;下面给出两种事例:
1)
function fa() { function fb() { alert("hello word"); } return fb; } var myfun = fa(); myfun();
2)
function fa() { var e = document.getElementById("id"); e.event = function () { alert("hello word"); }; }
这两种形式的造成闭包的机制不一样,一个经过一个return 返回这个内部函数,从而被包外引用,而另外一个则是经过 e,这个docment这个宿主对象的事件而完成外部引用。这两种形式造成的结果就是fa这个函数的的内部函数能够被fa的外部变量所引用,这就造成了闭包。
闭包讲到这里我想你们琢磨一下应该很清楚了,下面咱们来分析下这个内存泄漏是怎么造成的。不少资料都说循环引用,IE的计数式的垃圾回收机制,但我相信,这些概念很模糊,究竟是怎么们回事,咱们下面详细来剖析。
垃圾回收机制如今很成熟了,但早期的IE版本里(ie4-ie6),对宿主对象(也就是document对象)采用是计数的垃圾回收机制,闭包致使内存泄漏的一个缘由就是这个算法的一个缺陷。循环引用会致使无法回收,这个循环引用只限定于有宿主对象参与的循环引用,而js对象之间即便造成循环引用,也不会产生内存泄漏,由于对js对象的回收算法不是计数的方式。
首先咱们明确下内存泄漏的概念:内存里不能被回收也不能被利用的空间即为内存泄漏。为何不能被回收呢?不符合内存回收的算法;为何不能被利用呢?在栈上没有指向他的指针。在这里我简单的讲一下堆和栈的关系:
function fa() { var o = new Object(); } fa();
咱们看这段代码执行的时候发生了什么
咱们看到,栈上只是存了一个指针,指针就是堆上对象的的地址;咱们的程序经过这个指针句能够操做堆上的对象。栈上的这个指针是自动管理的,当函数退出后,就销毁了;这样程序就在没办法访问到堆上的这个对象了,而堆上的这个对象这个时候就会被咱们的GC自动回收了;若是回收不了,就是内存泄漏了。
讲到这里你们对内存泄露应该是有所了解了,对于计数回收方式你们查下资料,相信你们根据上面讲的应该能够看明白了,这里再也不详细描述。下面咱们着重描述下内存泄露的缘由,你们先看下面的代码:
function fa() { var a = "hello word"; return function () { alert(a); } } var o = fa(); o();
这段代码输出hello word,这说明什么?说明在堆上的”hello word‘ 没有被回收,什么缘由?由于o这个函数还要引用这个变量。下面咱们用计数的GC方式来逐句分析程序的代码函数
在堆上有两个对象 一个是 hello word 咱们叫作O1,匿名函数function(){alert(a);}咱们叫作O2编码
当执行
var a = "hello word"; 的时候 O1的计数为1;
当执行
return function () {
alert(a);
}
的时候 O2的计数变为1;
当执行完fa这个函数后 栈上的 var a 会被销毁,同是他指向的对象计数减1,这样问题就来了,这样O1的计数变为0了,那不被gc回收了嘛?怎么还会输出"hello word"?
原来在执行
return function () {
alert(a); }
这个函数的时候,为了保持函数对这个变量的引用,在这个匿名函数的做用域链上加了一个对O1的引用,这样 其实 O1的计数在变成了2,在a被销毁后,O1减变成了1而不是0.
那么O1时候被回收呢?当O2被回收的时候。O2何时被回收呢?当指向他的var o 从栈上消失的时候。
好,讲到这里,原理咱们讲完了下面咱们就看下
function fa() { var e = document.getElementById("id"); e.event = function () { alert("hello word"); }; }
这段代码为何会形成内存泄露spa
var e = document.getElementById("id"); 执行这段代码的时候 右边(O1)的对象计数变为了1
执行这段代码的时候指针
e.event = function () {
alert("hello word"); };
匿名函数(O2)的计数变了1;对象O1的计数变了2;code
当函数fa执行完毕时 栈上的指针var e 消失,他指向的对象 O1的计数减1变为了1;这样当函数执行完毕,O一、O2的这两个对象的计数都为1,根据计数的回收算法,就都留在内存里了不能被GC回收了,就形成了内存泄漏。htm
上面说法不彻底正确,实际上执行完fa后O2的计数是2,这个你们能够想一下缘由。对象
其实fa里面的宿主对象只是真正对象一个副本,当执行
e.event 这句指令的时候 作了两件事,一个是副本的对象指向O2 这时O2的计数加1,真正的宿主对象又指向这个O2,这个O2的计数再加1 变为了2
因此 在fa的外面执行 e.event=null 的时候,这时O2计数减1变为了1, 这时候,栈上再没有指向O2的指针了,因此O2的计数再没有减小的机会了。这样O2就永远存在了,O2存在,那么O2的做用域链指向O1的指针就永远存在了,因此O1也就永远存了,这样O一、O2 就再没机会释放了,就形成了内存泄漏。