JavaScript垃圾回收由引擎自动执行,采用标记-清除算法处理循环引用;WeakMap、WeakRef和FinalizationRegistry可辅助管理弱引用与清理逻辑;常见泄漏源于隐式强引用,需借助DevTools分析保留路径。

JavaScript 的垃圾回收不是你手动控制的,而是引擎自动运行的;它只回收「不再被引用」的对象,不会回收「还有变量指向它」的东西。哪怕你心里觉得“这数据没用了”,只要还存在隐式引用,就不会被收走。
标记-清除(Mark-and-Sweep)是主流实现方式
V8、SpiderMonkey、JavaScriptCore 等引擎都用这个基本策略:先标记所有「可访问」的对象(从全局对象、函数调用栈等根集出发遍历引用链),再清除所有未被标记的对象。
- 它能正确处理循环引用——比如
a.ref = b且b.ref = a,只要a和b都不再被全局或栈中变量引用,整块内存就会被一并回收 - 不依赖引用计数,所以避免了
IE6时代因循环引用导致 DOM 对象无法释放的经典内存泄漏 - 回收动作不是实时发生的,而是在内存压力上升或空闲时由引擎调度,因此你无法预测
delete或赋值为null后内存何时真正释放
WeakMap 和 WeakRef 是打破强引用的关键工具
它们允许你持有对象但不阻止 GC 回收——适合缓存、元数据绑定、事件监听器管理等场景。
-
WeakMap的键必须是对象,且不阻止该对象被回收;一旦键被回收,对应条目自动消失 -
WeakRef(ES2021)让你拿到一个弱引用句柄,需调用.deref()才可能取到原对象;如果已被回收,.deref()返回undefined - 不能对
WeakRef使用===判断是否相等,也不能用它做属性名或 Map 键 -
FinalizationRegistry可注册回调,在对象被回收后触发清理逻辑(比如关闭底层资源),但不保证立即执行,也不保证一定执行
常见泄漏模式和应对方式
大多数内存问题不是 GC 失效,而是你不小心维持了本不该存在的引用。
- 全局变量残留:
window.cacheData = hugeArray—— 改成局部作用域或显式赋值cacheData = null - 定时器未清除:
setInterval(() => {...}, 1000)中闭包捕获了大对象 → 清理时调用clearInterval(id),并确保闭包不意外保留对外部作用域的引用 - 事件监听器未解绑:使用
addEventListener后忘记removeEventListener,尤其在单页应用组件销毁时;推荐用{ once: true }或用AbortController控制信号 - 闭包中缓存 DOM 元素但未随元素移除而清理:改用
WeakMap关联状态,或监听DOMNodeRemoved(已废弃)/ 使用MutationObserver
const cache = new WeakMap();
function decorateElement(el) {
if (!cache.has(el)) {
cache.set(el, { timestamp: Date.now(), computedStyle: getComputedStyle(el) });
}
return cache.get(el);
}
// el 被从 DOM 移除后,cache 中对应项会自动消失
真正难调试的从来不是「GC 没运行」,而是「你以为它该被回收,其实还挂着一根看不见的引用」——比如一个被闭包捕获的父级作用域,或者某个第三方库内部保存的回调数组。用 Chrome DevTools 的 Memory 面板拍堆快照,按「Retained Size」排序,点开「Retainers」看谁还在 hold 它,比猜要可靠得多。










