常见的内存泄漏有哪些?
在 JavaScript 中,内存泄漏指的是应用程序不再需要某块内存,但由于某种原因,垃圾回收机制(GC, Garbage Collection)无法将其回收,导致内存占用持续升高,最终可能引发性能下降或崩溃。
以下是 JavaScript 中导致内存泄漏的最常见情况及示例:
1. 意外的全局变量
在 JavaScript 中,如果未声明的变量被赋值,它会自动成为全局对象的属性(浏览器中是 window,Node.js 中是 global)。全局变量在页面关闭前永远不会被垃圾回收。
function leak() {
// 忘记了使用 let/const/var
secretData = "这是一段敏感数据"; // 变成了 window.secretData
}
leak();
解决方案:
- 使用严格模式 (
'use strict') 来避免意外的全局变量。 - 使用完后手动设置为
null。
2. 被遗忘的定时器或回调函数
如果代码中设置了 setInterval 或 setTimeout,但忘记清除(clear),且定时器内部引用了外部变量,那么这些变量无法被释放。
const someResource = hugeData(); // 很大的数据
setInterval(function() {
// 这个回调引用了 someResource
console.log(someResource);
}, 1000);
// 如果没有调用 clearInterval,someResource 会一直留在内存中
解决方案:
- 在组件卸载或页面关闭时,清除定时器:
clearInterval(id)。
3. 闭包(Closures)的不当使用
闭包是 JavaScript 的强大特性,但如果闭包长期持有父函数的变量,而这些变量又很大,就会造成泄漏。
function outer() {
const largeArray = new Array(1000000).fill('data');
return function inner() {
// inner 函数引用了 outer 作用域的 largeArray
// 只要 inner 函数还存在,largeArray 就无法被回收
console.log(largeArray.length);
};
}
const innerFunc = outer(); // largeArray 被保留
// 如果后续没有释放 innerFunc,内存就会泄漏
解决方案:
- 确保不再需要的函数被释放(
innerFunc = null)。 - 在闭包外尽量避免引用大对象。
4. DOM 引用未被清理
当把 DOM 元素存储为 JavaScript 对象或数据结构时,即使该元素已从 DOM 树中移除,只要 JS 中还有引用,该 DOM 元素连同其事件监听器就不会被释放。
const elements = {
button: document.getElementById('button')
};
function removeButton() {
document.body.removeChild(document.getElementById('button'));
// 注意:elements.button 仍然指向那个 DOM 对象,所以它无法被回收
}
解决方案:
- 移除 DOM 节点后,同时将变量设置为
null。
5. 事件监听器未移除
向 DOM 元素添加了事件监听器,但在移除该元素前没有移除监听器。现代浏览器(尤其是针对原生 DOM 的监听器)处理得比以前好,但在单页应用(SPA, Single Page Application)中,如果频繁添加和移除元素,累积的监听器仍会导致泄漏。
const element = document.getElementById('button');
element.addEventListener('click', onClick);
// 如果后来 element 被移除了,但没有 removeEventListener
// 并且 onClick 函数引用了外部变量,就会造成泄漏
解决方案:
- 在移除元素前调用
removeEventListener。 - 使用框架(如 React、Vue)时,框架的生命周期通常会自动处理,但要注意在
useEffect的清理函数中移除原生监听器。
6. 脱离 DOM 树的引用(DOM 树内部引用)
这通常发生在给 DOM 元素添加自定义属性时。如果两个 DOM 元素相互引用,即使从文档流中移除,也可能因为循环引用导致泄漏(在老版本 IE 中常见,现代浏览器有所改进,但仍需注意)。
7. Map 或 Set 的不当使用
使用对象作为 Map 或 Set 的 key,如果只把 key 置为 null,而没有从 Map 中删除它,key 依然被 Map 引用着,无法被回收。
let obj = {};
const map = new Map();
map.set(obj, 'some value');
obj = null; // 这里 obj 被置为 null
// 但 map 里仍然有对原对象的引用,所以原对象无法被回收
解决方案:
- 使用
WeakMap和WeakSet。它们的 key 是弱引用,不会阻止垃圾回收。
8. console.log 的影响
在开发环境调试时打印对象,如果线上环境忘记删除 console.log,控制台会一直持有对象的引用(特别是打印复杂对象时),导致对象无法被回收。现代浏览器在处理 console.log 时有所优化,但仍需注意。
建议:
- 生产环境打包时移除所有
console.log。
总结:如何避免内存泄漏?
-
使用
WeakMap和WeakSet存储对象引用。 - 及时清理:清除定时器、取消订阅、解绑事件。
-
避免全局变量,使用
let/const和严格模式。 - 合理使用闭包,避免在闭包中持有大量数据的引用。
-
善用工具:
- 使用 Chrome DevTools 的 Memory 面板拍摄堆快照(Heap Snapshot),分析内存占用。
- 使用 Performance 面板监控内存变化。