“阻止 GC” 是 JavaScript 内存管理中的一个核心概念。
简单来说:只要你的变量(Map/Set)里还“抓着”某个对象不放,垃圾回收器(GC)就不敢把这个对象删掉,哪怕这个对象在页面上已经没用了。
为了让你彻底明白,我们需要拆解一下 GC(垃圾回收) 的工作原理和 强引用 vs 弱引用 的区别。
1. 什么是 GC (Garbage Collection)?
JavaScript 引擎(如 Chrome 的 V8)有一个自动机制叫 垃圾回收器 (GC)。 它的任务很简单:定期扫描内存,把那些“没人用”的对象清理掉,释放内存空间。
GC 判断“没人用”的标准(标记 – 清除算法):
- 如果一个对象没有任何变量引用它(即它是“不可达”的),GC 就认为它是垃圾,可以删除。
- 如果一个对象至少被一个变量引用着(即它是“可达”的),GC 就认为它还在被使用,绝对不能删。
2. 什么是“阻止 GC”?(强引用的代价)
当你使用普通的 Map、Set、数组或对象属性来存储另一个对象时,你建立的是一个 强引用 (Strong Reference)。
这意味着: 只要 Map 还活着,Map 里面存的那个对象就永远被视为“正在被使用”。即使那个对象在业务逻辑上已经废了(比如对应的 DOM 元素被删除了,或者用户跳转页面了),GC 也不敢动它。
🚨 内存泄漏现场演示
假设你有一个全局的缓存 userCache (Map),用来存用户信息对象:
// 全局 Map,生命周期贯穿整个应用
const userCache = new Map();
function loadUser(userId) {
// 模拟从服务器获取一个巨大的用户对象
const userObj = { id: userId, data: new Array(1000000).fill('x') };
// 【关键动作】把 userObj 存入 Map
userCache.set(userId, userObj);
return userObj;
}
function deleteUser(userId) {
// 用户注销了,或者页面关闭了
// 此时,业务代码里没有任何变量再指向那个 userObj 了
// 理论上,userObj 应该被 GC 回收
// ❌ 但是!userCache 这个 Map 依然死死地抓着 userObj 不放!
// 对 GC 来说:userCache -> userObj,这条路是通的。
// 结论:userObj 不是垃圾,不能删!
// 如果你忘记写 userCache.delete(userId),内存泄漏就发生了。
// 随着用户不断登录注销,userCache 会越来越大,直到浏览器崩溃。
}
这就是“阻止 GC”: 因为 Map 的强引用特性,它强行延长了内部对象的寿命,导致本该被回收的内存无法释放。
3. WeakMap/WeakSet 是如何“不阻止 GC”的?(弱引用)
WeakMap 和 WeakSet 建立的是 弱引用 (Weak Reference)。
这意味着: WeakMap 对里面的对象只是“轻轻搭了一把手”,并没有“死死抓住”。
- GC 的逻辑变了:当 GC 扫描时,它会发现:“咦,除了 WeakMap 里有个弱引用指向这个对象外,其他地方都没有人引用它了。”
- GC 的决定:“既然没别人用它了,那它就是垃圾!” -> 直接回收该对象。
- 后续:一旦对象被回收,WeakMap 里对应的这条记录也会自动消失(就像从来没存在过一样)。
✅ 修复上面的例子
// 改用 WeakMap
const userCache = new WeakMap();
// 注意:WeakMap 的键必须是对象,这里假设 userId 是个对象或者我们用 userObj 本身做键
// 为了演示,我们假设 key 是 userObj 本身,或者某个关联的 DOM 节点
function loadUser(userObj) {
const data = { info: '…' };
// 【关键动作】弱引用存入
userCache.set(userObj, data);
return userObj;
}
function deleteUser(userObj) {
// 用户注销,外部没有变量引用 userObj 了
// let user = loadUser(…);
// user = null;
// GC 扫描发现:userObj 只有 WeakMap 里的弱引用,没有强引用了。
// GC 动作:回收 userObj!
// 连锁反应:WeakMap 中对应的 entry 自动清除。
// ✅ 完美!不需要手动 delete,内存自动释放,永不泄漏。
}
4. 形象比喻
-
普通 Map (强引用) 就像 手铐。
- 你把犯人(对象)拷在柱子(Map)上。
- 只要柱子还在,犯人就必须待在那,警察(GC)不能把他带走,哪怕犯人已经刑满释放了。
- 如果你忘了主动打开手铐(.delete()),犯人就会一直被关着(内存泄漏)。
-
WeakMap (弱引用) 就像 影子。
- 犯人(对象)走到哪,影子(WeakMap 的记录)就跟到哪。
- 但是影子抓不住人。
- 一旦犯人离开了(没有其他强引用了),消失了,影子也会瞬间自动消失。
- 你不需要专门去消灭影子。
5. 总结对比
| 对 GC 的影响 | 阻止 GC。只要容器在,内容就在。 | 不阻止 GC。如果外部没引用,内容会被 GC 杀掉。 |
| 内存安全性 | 低。容易因忘记删除导致泄漏。 | 高。自动清理,防泄漏神器。 |
| 适用场景 | 需要长期持有数据、配置表、字典。 | 临时关联数据、缓存、DOM 绑定、私有属性。 |
| 副作用 | 必须手动管理生命周期 (.delete)。 | 数据可能“莫名其妙”消失(因为被 GC 了),所以不能依赖它做持久化存储。 |
一句话总结: “阻止 GC”意味着强制续命。如果你希望某个对象在没人用时赶紧死掉(释放内存),就不要用普通 Map 存它,而要用 WeakMap。
实操补充:如何验证是否被 GC 了?
不要猜,用 Chrome DevTools 看真相:
- 点击 Take snapshot (快照 1)。
- 执行你的操作:进入页面 -> 离开页面(重复几次,制造疑似泄漏)。
- 点击 Take snapshot (快照 2)。
- 点击 Take snapshot (快照 3) (为了触发 GC,有时需要多点几次)。
- 在快照 2 或 3 中,选择 Comparison (对比) 模式,对比快照 1。
- 搜索你的组件名或变量名(如 bigData)。
- 如果看到 Delta (增量) 是正数,且 Distance (距离) 显示它被某个全局对象(如 Window, Map, Closure)引用着,那就是泄漏了。
- 点击引用链(Retainers),你会清晰地看到是谁(哪行代码)抓着它不放。
| 纯局部变量 (data, ref, computed) | ✅ 会 | 组件卸载后,无外部引用,自动回收。 |
| 存入全局 Map/Object | ❌ 不会 | 除非手动 .delete(),否则强引用一直存在。建议改用 WeakMap。 |
| 定时器/监听器未清理 | ❌ 不会 | 闭包持有引用。务必在 onUnmounted 清理。定时器和监听器的使用会对是否GC有影响,请查看核心结论 |
| DOM 元素引用 | ❌ 不会 | 如果你在 JS 里存了 document.getElementById 的结果,即使 DOM 删了,JS 变量还指着它。建议用 WeakMap 存 DOM 关联数据。 |
| Vue 模板语法 (@click) | ✅ 会 | Vue 内部管理良好,销毁时会自动清理。 |
| Vue 模板指令 (v-if, v-for) | ✅ 会 | Vue 内部管理良好,销毁时会自动清理。 |
核心结论: Vue 框架本身做得很好,大部分泄漏都是开发者自己写的“副作用代码”(定时器、全局缓存、事件监听)导致的。 只要遵循 “谁创建,谁销毁” 的原则,在 unmounted / onUnmounted 中清理干净副作用,你的对象就能被正常 GC。
对于定时器 (setInterval):如果你用箭头函数(原因:箭头函数的特性是词法作用域Lexical This。它在定义的那一刻,就永久绑定了外部环境mounted 函数的 this),即使没用到 this,它依然会持有 this(导致泄漏)。如果你用普通函数且没用到 this,则不会持有,可以被GC。 对于事件监听 (addEventListener):逻辑同上。关键在于你定义回调函数的方式(箭头函数 vs 普通函数)以及它定义的作用域。
补充技巧1:自定义指令或 Composables
如果你发现到处都在写 addEventListener 和 removeEventListener,可以封装一个 Composable:
// useEventListener.js
import { onUnmounted } from 'vue';
export function useEventListener(target, event, handler) {
target.addEventListener(event, handler);
// 自动注册清理逻辑
onUnmounted(() => {
target.removeEventListener(event, handler);
});
}
// 组件中使用
setup() {
const handler = () => {};
useEventListener(window, 'resize', handler);
// 组件卸载时自动清理,无需手动写 removeEventListener
}




