JavaScript垃圾回收采用标记-清除机制,从根对象出发标记可达对象,未标记者被清除;常见泄漏源包括未解绑事件监听器、未清除定时器、闭包过度捕获及缓存未释放,需用Chrome Memory面板分析堆快照定位悬空引用。

JavaScript 的垃圾回收不靠你手动 delete 或 null,而靠引擎自动判断“这个对象还能不能被访问到”。只要它还能从根(比如 window、当前函数的局部变量)顺着引用链摸到,就不会被回收——哪怕你已经把它设为 null,只要别的地方还留着引用,它就照活不误。
标记清除是核心机制,不是引用计数
现代引擎(V8、SpiderMonkey)全用标记-清除(Mark-and-Sweep),不是引用计数。后者早被淘汰,因为它搞不定循环引用:两个对象互相 a.ref = b; b.ref = a,但外部没人再用它们,引用计数永远是 1,内存就卡死不放。
标记清除分两步:
- 标记阶段:从根出发,递归遍历所有能触达的对象,打上“活跃”标记
- 清除阶段:把没被标记的对象整块内存释放
这个过程不是实时的,而是在空闲或内存压力大时触发,所以你看到内存曲线有延迟回落,属正常。
立即学习“Java免费学习笔记(深入)”;
事件监听器没配对移除,DOM 就悬在半空
这是最常见泄漏点:元素已从 DOM 中 removeChild 或被框架卸载,但 addEventListener 绑定的监听器还在,尤其用匿名函数或闭包时,整个作用域链都被锁住。
典型错误写法:
element.addEventListener('click', () => {
console.log(this.data); // 闭包捕获了 this 和大量数据
});
正确做法:
- 用具名函数 +
removeEventListener配对清理 - 组件级开发中,在
useEffect返回函数(React)或beforeUnmount(Vue)里统一解绑 - 优先用
{ once: true }选项,适合只触发一次的场景 - 对动态节点,改用事件委托,避免逐个绑定
定时器和闭包联手“绑架” this 和大数组
setInterval 回调里用了 this.state 或 largeList,组件卸载后定时器还在跑?那整个组件实例和它闭包里的所有数据都动不了。
同样,循环中定义函数也危险:
for (let i = 0; i < 1000; i++) {
elements[i].addEventListener('click', () => {
console.log(dataList); // 每次都捕获整个 dataList,不是 item.id
});
}
结果:1000 个闭包全锁住 dataList,它永远不可达不了。
改进方式:
- 组件销毁前必须调用
clearInterval(id)或clearTimeout(id) - 闭包里只取真正需要的值,比如
const id = item.id; handler = () => console.log(id) - 大型缓存对象不用时,显式设为
cache = null,别只清空数组arr.length = 0 - 考虑用
WeakMap存元数据:键是 DOM 元素,值不会阻止元素被回收
排查泄漏不能靠猜,得看堆快照
光靠代码检查容易漏,必须用工具验证。Chrome DevTools 的 Memory 面板 是主力:
- 操作前拍一个堆快照(Heap Snapshot),操作后(比如跳转页面、关闭弹窗)再拍一个,用
Comparison视图对比 - 重点筛:
Detached DOM tree(游离 DOM)、重复增长的Array/Object/ 自定义类实例 - 点开可疑对象,看右侧
Retainers标签,顺引用链往上找谁在拽着它不放 - 配合
Allocation Timeline录制,观察某段 JS 执行是否持续分配却不释放
真正难的不是理解 GC 原理,而是发现那些“看起来已经删了,其实还被某个闭包、定时器、全局属性悄悄攥着”的引用链。











