JavaScript 中的内存泄漏

  JavaScript 是一种垃圾收集式语言,这就是说,内存是根据对象的创建分配给该对象的,并会在没有对该对象的引用时由浏览器收回。JavaScript 的垃圾收集机制本身并没有问题,但浏览器在为 DOM 对象分配和恢复内存的方式上却有些出入。

  Internet Explorer 和 Mozilla Firefox 均使用引用计数来为 DOM 对象处理内存。在引用计数系统,每个所引用的对象都会保留一个计数,以获悉有多少对象正在引用它。如果计数为零,该对象就会被销毁,其占用的内存也会返回 给堆。虽然这种解决方案总的来说还算有效,但在循环引用方面却存在一些盲点。

循环引用的问题何在?

  当两个对象互相引用时,就构成了循环引用,其中每个对象的引用计数值都被赋 1。在纯垃圾收集系统中,循环引用问题不大:若涉及到的两个对象中的一个对象被任何其他对象引用,那么这两个对象都将被垃圾收集。而在引用计数系统,这两 个对象都不能被销毁,原因是引用计数永远不能为零。在同时使用了垃圾收集和引用计数的混合系统中,将会发生泄漏,因为系统不能正确识别循环引用。在这种情 况下,DOM 对象和 JavaScript 对象均不能被销毁。清单 1 显示了在 JavaScript 对象和 DOM 对象间存在的一个循环引用。

清单 1. 循环引用导致了内存泄漏

1 <script type="text/javascript">
 2 document.write("circular references between JavaScript and DOM!");
 3 var obj;
 4 window.onload = function(){
 5 obj=document.getElementById("DivElement");
 6         document.getElementById("DivElement").expandoProperty=obj;
 7         obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
 8         };
 9 </script>
10 <div id="DivElement">Div Element</div>

  如上述清单中所示,JavaScript 对象 obj 拥有到 DOM 对象的引用,表示为 DivElement 。而 DOM 对象则有到此 JavaScript 对象的引用,由 expandoProperty 表示。可见,JavaScript 对象和 DOM 对象间就产生了一个循环引用。由于 DOM 对象是通过引用计数管理的,所以两个对象将都不能销毁。

 

另一种内存泄漏模式

  在清单 2 中,通过调用外部函数 myFunction 创建循环引用。同样,JavaScript 对象和 DOM 对象间的循环引用也会导致内存泄漏。

清单 2. 由外部函数调用引起的内存泄漏

1 <script type="text/javascript">
 2 document.write(" object s between JavaScript and DOM!");
 3 function myFunction(element)
 4 {
 5     this.elementReference = element;
 6     // This code forms a circular reference here
 7     //by DOM-->JS-->DOM
 8     element.expandoProperty = this;
 9 }
10 
11 function Leak() {
12     //This code will leak
13     new myFunction(document.getElementById("myDiv"));
14 }
15 </script>
16 
17 <body onload="Leak()">
18 <div id="myDiv"></div>

  正如这两个代码示例所示,循环引用很容易创建。在 JavaScript 最为方便的编程结构之一:闭包中,循环引用尤其突出。

 

JavaScript 中的闭包

  JavaScript 的过人之处在于它允许函数嵌套。一个嵌套的内部函数可以继承外部函数的参数和变量,并由该外部函数私有。清单 3 显示了内部函数的一个示例。

清单 3. 一个内部函数

1 function parentFunction(paramA){
2     var a = paramA;
3     function childFunction()    {
4         return a + 2;
5     }
6     return childFunction();
7 }

  JavaScript 开发人员使用内部函数来在其他函数中集成小型的实用函数。如清单 3 所示,此内部函数 childFunction 可以访问外部函数 parentFunction 的变量。当内部函数获得和使用其外部函数的变量时,就称其为一个闭包 。

了解闭包

考虑如清单 4 所示的代码片段。

清单 4. 一个简单的闭包

1 <script type="text/javascript">
 2 document.write("Closure Demo!!");
 3 window.onload=
 4 function  closureDemoParentFunction(paramA)
 5 {
 6     var a = paramA;
 7     return function closureDemoInnerFunction (paramB)
 8     {
 9             alert( a +" "+ paramB);
10     };
11 };
12 var x = closureDemoParentFunction("outer x");
13 x("inner x");
14 </script>

  在上述清单中,closureDemoInnerFunction 是在父函数 closureDemoParentFunction 中定义的内部函数。当用外部的 x 对 closureDemoParentFunction 进行调用时,外部函数变量 a 就会被赋值为外部的 x 。函数会返回指向内部函数 closureDemoInnerFunction 的指针,该指针包括在变量 x 内。

  外部函数 closureDemoParentFunction 的本地变量 a 即使在外部函数返回时仍会存在。这一点不同于 C/C++ 这样的编程语言,在 C/C++ 中,一旦函数返回,本地变量也将不复存在。在 JavaScript 中,在调用 closureDemoParentFunction 的时候,带有属性 a 的范围对象将会被创建。该属性包括值 paramA ,又称为“外部 x” 。同样地,当 closureDemoParentFunction 返回时,它将会返回内部函数 closureDemoInnerFunction ,该函数包括在变量 x 中。

  由于内部函数持有到外部函数的变量的引用,所以这个带属性 a 的范围对象将不会被垃圾收集。当对具有参数值inner x 的 x 进行调用时,即 x("inner x") ,将会弹出警告消息,表明 “outer x innerx ”。

  清单 4简要解释了 JavaScript 闭包。闭包功能非常强大,原因是它们使内部函数在外部函数返回时也仍然可以保留对此外部函数的变量的访问。不幸的是,闭包非常易于隐藏 JavaScript 对象 和 DOM 对象间的循环引用。

 

闭包和循环引用

  在清单 5 中,可以看到一个闭包,在此闭包内,JavaScript 对象(obj )包含到 DOM 对象的引用(通过 id "element" 被引用)。而 DOM 元素则拥有到 JavaScript obj 的引用。这样建立起来的 JavaScript 对象和 DOM 对象间的循环引用将会导致内存泄漏。

清单 5. 由事件处理引起的内存泄漏模式

1 <script type="text/javascript">
 2 document.write("Program to illustrate memory leak via closure");
 3 window.onload=function outerFunction(){
 4     var obj = document.getElementById("element");
 5     obj.onclick=function innerFunction(){
 6     alert("Hi! I will leak");
 7     };
 8     obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
 9     // This is used to make the leak significant
10 };
11 </script>
12 <button id="element">Click Me</button>

 

避免内存泄漏

  幸好,JavaScript 中的内存泄漏是可以避免的。当确定了可导致循环引用的模式之后,正如我们在上述章节中所做的那样,您就可以开始着手应对这些模式了。这里,我们将以上述的 由事件处理引起的内存泄漏模式为例来展示三种应对已知内存泄漏的方式。

  一种应对 清单 5中的内存泄漏的解决方案是让此 JavaScript 对象 obj 为空,这会显式地打破此循环引用,如清单 6 所示。

清单 6. 打破循环引用

1 <script type="text/javascript">
 2 document.write("Avoiding memory leak via closure by breaking the circular
 3 reference");
 4     window.onload=function outerFunction(){
 5     var obj = document.getElementById("element");
 6     obj.onclick=function innerFunction()
 7     {
 8         alert("Hi! I have avoided the leak");
 9         // Some logic here
10     };
11     obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
12     obj = null
13 
14 
15 
16 ; //This breaks the circular reference
17     };
18 </script>
19 <button id="element">"Click Here"</button>

 

清单 7 是通过添加另一个闭包来避免 JavaScript 对象和 DOM 对象间的循环引用。

清单 7. 添加另一个闭包

1 <script type="text/javascript">
 2 document.write("Avoiding a memory leak by adding another closure");
 3 window.onload=function outerFunction(){
 4 var anotherObj = function innerFunction()
 5          {
 6             // Some logic here
 7             alert("Hi! I have avoided the leak");
 8          };
 9      (function anotherInnerFunction(){
10         var obj =  document.getElementById("element");
11         obj.onclick=anotherObj })();
12 
13 
14 
15 
16         };
17 </script>
18 <button id="element">"Click Here"</button>

 

清单 8 则通过添加另一个函数来避免闭包本身,进而阻止了泄漏。

清单 8. 避免闭包自身

1 <script type="text/javascript">
 2 document.write("Avoid leaks by avoiding closures!");
 3 window.onload=function(){
 4     var obj = document.getElementById("element");
 5     obj.onclick = doesNotLeak;
 6 }
 7 
 8 function doesNotLeak(){
 9     //Your Logic here
10     alert("Hi! I have avoided the leak");
11 }
12 
13 </script>
14 
15 <button id="element">"Click Here"</button>

 


结束语

  本文解释了循环引用是如何导致 JavaScript 中的内存泄漏的 —— 尤其是在结合了闭包的情况下。您还了解了涉及到循环引用的一些常见内存泄漏模式以及应对这些泄漏模式的几种简单方式