常见的内存泄漏有哪些?

0 阅读4分钟

在 JavaScript 中,内存泄漏指的是应用程序不再需要某块内存,但由于某种原因,垃圾回收机制(GC, Garbage Collection)无法将其回收,导致内存占用持续升高,最终可能引发性能下降或崩溃。

以下是 JavaScript 中导致内存泄漏的最常见情况及示例:

1. 意外的全局变量

在 JavaScript 中,如果未声明的变量被赋值,它会自动成为全局对象的属性(浏览器中是 window,Node.js 中是 global)。全局变量在页面关闭前永远不会被垃圾回收。

function leak() {
  // 忘记了使用 let/const/var
  secretData = "这是一段敏感数据"; // 变成了 window.secretData
}
leak();

解决方案:

  • 使用严格模式 ('use strict') 来避免意外的全局变量。
  • 使用完后手动设置为 null

2. 被遗忘的定时器或回调函数

如果代码中设置了 setIntervalsetTimeout,但忘记清除(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 的不当使用

使用对象作为 MapSet 的 key,如果只把 key 置为 null,而没有从 Map 中删除它,key 依然被 Map 引用着,无法被回收。

let obj = {};
const map = new Map();
map.set(obj, 'some value');

obj = null; // 这里 obj 被置为 null
// 但 map 里仍然有对原对象的引用,所以原对象无法被回收

解决方案:

  • 使用 WeakMapWeakSet。它们的 key 是弱引用,不会阻止垃圾回收。

8. console.log 的影响

在开发环境调试时打印对象,如果线上环境忘记删除 console.log,控制台会一直持有对象的引用(特别是打印复杂对象时),导致对象无法被回收。现代浏览器在处理 console.log 时有所优化,但仍需注意。

建议:

  • 生产环境打包时移除所有 console.log

总结:如何避免内存泄漏?

  1. 使用 WeakMapWeakSet 存储对象引用。
  2. 及时清理:清除定时器、取消订阅、解绑事件。
  3. 避免全局变量,使用 let/const 和严格模式。
  4. 合理使用闭包,避免在闭包中持有大量数据的引用。
  5. 善用工具
    • 使用 Chrome DevTools 的 Memory 面板拍摄堆快照(Heap Snapshot),分析内存占用。
    • 使用 Performance 面板监控内存变化。