即使有垃圾回收机制,JavaScript 应用中仍可能出现内存泄漏——即不再需要的内存因仍被引用而无法释放。长期运行的应用(尤其是单页应用或 Node.js 服务)若存在内存泄漏,会逐渐占用越来越多内存,最终导致卡顿甚至崩溃。理解常见泄漏场景并掌握排查方法,是前端工程化能力的重要一环。
7.4.1 常见内存泄漏场景
1. 意外的全局变量
在非严格模式下,未声明的变量会自动成为全局对象的属性,如果这些变量持有大块数据,就会一直占用内存。
function leak() {
hugeData = new Array(10000000); // 未用 var/let/const,变成全局变量
}
如何避免:始终使用 let、const 声明变量,并开启严格模式('use strict')。
2. 被遗忘的定时器或回调
setInterval 或 setTimeout 引用了 DOM 元素或闭包中的数据,如果之后没有清除,即使 DOM 移除了,回调仍然持有引用,导致内存无法回收。
const element = document.getElementById('box');
setInterval(() => {
console.log(element.innerHTML); // element 一直被引用
}, 1000);
// 即使 element 从 DOM 中删除了,定时器依然存在,element 无法被 GC
如何避免:在合适的生命周期(如组件卸载、页面离开)中调用 clearInterval / clearTimeout。
3. 脱离 DOM 树的引用
有时 JavaScript 变量保留了对已移除 DOM 节点的引用,这些 DOM 节点虽然不在页面上,但内存中仍然存在。
const elements = [];
document.getElementById('btn').addEventListener('click', () => {
const div = document.getElementById('div');
elements.push(div); // 存储了 DOM 引用
div.parentNode.removeChild(div); // 从 DOM 树移除
// 但 div 仍被 elements 数组引用,无法回收
});
4. 闭包造成的不必要保留
闭包会让函数记住其定义时的作用域,如果闭包引用了一个很大的外部变量,而这个变量实际上并不需要,就会造成过度保留。
function outer() {
const bigData = new Array(10000000); // 大数组
return function inner() {
console.log('hello');
// inner 并没有用到 bigData,但闭包持有了整个 external 作用域
};
}
const fn = outer();
// bigData 仍然无法被回收,因为 inner 闭包引用了它
优化:将不需要的变量置为 null,或使用更小粒度的作用域分割。
5. 事件监听器未清理
在单页应用中,如果在全局对象或祖先元素上绑定了事件,而页面切换后监听器仍然存在,不仅造成内存泄漏,还会导致意外触发。
window.addEventListener('scroll', handler);
// 路由切换后没有 removeEventListener
6. 循环引用
老旧浏览器(IE6/7)的垃圾回收器无法处理 JavaScript 对象与 DOM 对象之间的循环引用,现代引擎已解决该问题,但理解循环引用有助于分析引用关系。
var elem = document.getElementById('elem');
elem.someData = { big: new Array(1000000) };
// elem 和 someData 互相引用,现代 GC 使用标记清除可处理
7.4.2 内存泄漏的排查思路
1. 使用浏览器开发者工具——Performance 面板
- 在 Chrome 中打开 Performance 面板,勾选 “Memory” 选项。
- 点击录制,执行可疑操作,停止录制。
- 观察 JS Heap 的走势:如果呈“阶梯状”持续上升且不回落,很可能存在泄漏。
2. 使用 Memory 面板(堆快照)
- 在 Memory 面板中,选择 “Heap snapshot”(堆快照),拍摄一个初始快照。
- 执行一系列操作(如打开/关闭弹窗、切换路由)后再拍摄第二个快照。
- 在快照对比视图中,按 “Delta” 排序,重点关注新增的对象数量和 “Shallow Size”。
- 在对象列表中可看到构造函数名称,定位到具体业务代码中的对象。
3. 使用 Performance monitor(实时监控)
- 在 Chrome 按
Ctrl+Shift+P输入 “Show Performance monitor”,打开实时性能监控。 - 查看 “JS heap size” 是否持续增长。
4. 定位分离的 DOM 节点
堆快照中可以按 “Detached DOM tree” 过滤,找到仍被 JavaScript 引用但已从页面移除的 DOM 节点,对应到代码中的变量,即可修复。
5. Node.js 环境排查
- 使用
node --inspect启动应用,在 Chrome DevTools 中连接调试,同样可拍摄堆快照。 - 使用
process.memoryUsage()输出内存使用情况。 - 第三方工具
heapdump可在运行时生成堆快照文件,导入 Chrome 分析。
7.4.3 预防内存泄漏的最佳实践
- 统一生命周期管理:组件卸载或页面销毁时,清理所有定时器、事件监听、请求、动画帧。
- 使用弱引用:
WeakMap、WeakSet存储对象键时不会阻止垃圾回收。 - 避免全局数据堆积:定期清理缓存、限制历史数据长度。
- 代码审查:关注闭包、事件绑定、定时器的清理逻辑。
内存泄漏通常不会在开发阶段显现,但在长期运行或低端设备上会成为严重问题。养成定期检查和主动清理的习惯,是成熟开发者的标志。