四种常见的JS内存泄漏
1、意外的全局变量
未定义的变量会在全局对象创建一个新变量,如下。
1 | function foo(arg) { |
函数foo
内部忘记使用var
,实际上JS会把bar挂载到全局对象上,意外创建一个全局变量。
1 | function foo(arg) { |
另一个意外的全局变量可能由this
创建。
1 | function foo() { |
解决方法:
在 JavaScript 文件头部加上'use strict'
,使用严格模式避免意外的全局变量,此时**上例中的this指向undefined
**。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。
被遗忘的计时器或回调函数
计时器setInterval
代码很常见
1 | var someResource = getData(); |
上面的例子表明,在节点node或者数据不再需要时,定时器依旧指向这些数据。所以哪怕当node节点被移除后,interval 仍旧存活并且垃圾回收器没办法回收,它的依赖也没办法被回收,除非终止定时器。
1 | var element = document.getElementById('button'); |
对于上面观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。因为老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。
但是,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经可以正确检测和处理循环引用了。即回收节点内存时,不必非要调用removeEventListener
了
3、脱离 DOM 的引用
如果把DOM 存成字典(JSON 键值对)或者数组,此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。那么将来需要把两个引用都清除。
1 | var elements = { |
如果代码中保存了表格某一个<td>
的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的<td>
以外的其它节点。实际情况并非如此:此<td>
是表格的子节点,子元素与父元素是引用关系。由于代码保留了<td>
的引用,导致整个表格仍待在内存中。所以保存 DOM 元素引用的时候,要小心谨慎。
4、闭包
闭包的关键是匿名函数可以访问父级作用域的变量。
1 | var theThing = null; |
每次调用replaceThing
,theThing
得到一个包含一个大数组和一个新闭包(someMethod
)的新对象。同时,变量unused
是一个引用originalThing
的闭包(先前的replaceThing
又调用了theThing
)。someMethod
可以通过theThing
使用,someMethod
与unused
分享闭包作用域,尽管unused
从未使用,它引用的originalThing
迫使它保留在内存中(防止被回收)。
解决方法:
在replaceThing
的最后添加originalThing = null
。