深入浅出JavaScript内存泄漏(1)(3)
Figure 2. 闭包函数引起的循环引用
普通的循环引用,是两个不可探知的对象相互引用造成的,但是闭包却不同。代替直接造成引用,闭包函数则取而代之从其父函数作用域中引入信息。通常,函数的 局部变量和参数只能在该被调函数自身的生命周期里使用。当存在闭包函数后,这些变量和参数的引用会和闭包函数一起存在,但由于闭包函数可以超越其父函数的 生命周期而存在,所以父函数中的局部变量和参数也仍然能被访问。
在下面的示例中,参数1将在函数调用终止时正常被释放。当我们加入了一个闭包函数后,一个 额外的引用产生,并且这个引用在闭包函数释放前都不会被释放。如果你碰巧将闭包函数放入了事件之中,那么你不得不手动从那个事件中将其移出。如果你把闭包 函数作为了一个expando属性,那么你也需要通过置null将其清除。
同时闭包会在每次调用中创建,也就是说当你调用包含闭包的函数两次,你将得到两个独立的闭包,而且每个闭包都分别拥有对参数的引用。由于这些显而易见的因 素,闭包确实非常用以带来泄漏。下面的示例将展示使用闭包的主要泄漏因素:
- <html>
- <head>
- <script language="JavaScript">
- function AttachEvents(element)
- {
- // This structure causes element to ref ClickEventHandler
- element.attachEvent("onclick", ClickEventHandler);
- function ClickEventHandler()
- {
- // This closure refs element
- }
- }
- function SetupLeak()
- {
- // The leak happens all at once
- AttachEvents(document.getElementById("LeakedDiv"));
- }
- function BreakLeak()
- {
- }
- </script>
- </head>
- <body onload="SetupLeak()" onunload="BreakLeak()">
- <div id="LeakedDiv"></div>
- </body>
- </html>
如果你对怎么避免这类泄漏感到疑惑,我将告诉你处理它并不像处理普通循环引用那么简单。"闭包"被看作函数作用域中的一个临时对象。一旦函数执行退出,你 将失去对闭包本身的引用,那么你将怎样去调用detachEvent方法来清除引用呢?在Scott Isaacs的MSN Spaces上有一种解决这个问题的有趣方法。
这个方法使用一个额外的引用(原文叫second closure,可是这个示例里致始致终只有一个closure)协助window对象执行onUnload事件,由于这个额外的引用和闭包的引用存在于 同一个对象域中,于是我们可以借助它来释放事件引用,从而完成引用移除。为了简单起见我们将闭包的引用暂存在一个expando属性中,下面的示例将向你 演示释放事件引用和清除expando属性。
- <html>
- <head>
- <script language="JavaScript">
- function AttachEvents(element)
- {
- // In order to remove this we need to put
- // it somewhere. Creates another ref
- element.expandoClick = ClickEventHandler;
- // This structure causes element to ref ClickEventHandler
- element.attachEvent("onclick", element.expandoClick);
- function ClickEventHandler()
- {
- // This closure refs element
- }
- }
- function SetupLeak()
- {
- // The leak happens all at once
- AttachEvents(document.getElementById("LeakedDiv"));
- }
- function BreakLeak()
- {
- document.getElementById("LeakedDiv").detachEvent("onclick",
- document.getElementById("LeakedDiv").expandoClick);
- document.getElementById("LeakedDiv").expandoClick = null;
- }
- </script>
- </head>
- <body onload="SetupLeak()" onunload="BreakLeak()">
- <div id="LeakedDiv"></div>
- </body>
- </html>
在这篇文章中,实际上建议我们除非迫不得已尽量不要创建使用闭包。文章中的示例,给我们演示了非闭包的事件引用方式,即把闭包函数放到页面的全局作用 域中。当闭包函数成为普通函数后,它将不再继承其父函数的参数和局部变量,所以我们也就不用担心基于闭包的循环引用了。在非必要的时候不使用闭包这样的编 程方式可以尽量使我们的代码避免这样的问题。
最后,脚本引擎开发组的Eric Lippert,给我们带来了一篇关于闭包使用通俗易懂的好文章。他的最终建议也是希望在真正必要的时候才使用闭包函数。虽然他的文章没有提及闭包会使用 的真正场景,但是这儿已有的大量示例非常有助于大家起步。
页面交叉泄漏(Cross-Page Leaks)
这种基于插入顺序而常常引起的泄漏问题,主要是由于对象创建过程中的临时对象未能被及时清理和释放造成的。它一般在动态创建页面元素,并将其添加到页面 DOM中时发生。一个最简单的示例场景是我们动态创建两个对象,并创建一个子元素和父元素间的临时域(译者注:这里的域(Scope)应该是指管理元素之 间层次结构关系的对象)。
然后,当你将这两个父子结构元素构成的的树添加到页面DOM树中时,这两个元素将会继承页面DOM中的层次管理域对象,并泄漏之 前创建的那个临时域对象。下面的图示示例了两种动态创建并添加元素到页面DOM中的方法。在第一种方法中,我们将每个子元素添加到它的直接父元素中,最后 再将创建好的整棵子树添加到页面DOM中。当一些相关条件合适时,这种方法将会由于临时对象问题引起泄漏。在第二种方法中,我们自顶向下创建动态元素,并 使它们被创建后立即加入到页面DOM结构中去。由于每个被加入的元素继承了页面DOM中的结构域对象,我们不需要创建任何的临时域。这是避免潜在内存泄漏 发生的好方法。






