------小Dora 的 JavaScript 修炼日记 · Day 9
"闭包让变量活得更久,WeakMap 让你不会背上内存债。"
------一位调试 Chrome Heap Snapshot 的前端工程师
🗂 目录
- Map 与 WeakMap:表面区别 vs 内存差距
- 什么是弱引用?为什么 WeakMap 是弱引用?
- V8 内存结构:堆、栈、隐藏的引用网络
- Map 和 WeakMap 在 V8 的底层实现(ephemeron table 黑科技)
- 闭包 + Map = 内存泄漏?WeakMap 如何破局
- 柯里化与缓存实战:Map vs WeakMap 对比
- GC 工作原理:为什么 WeakMap 条目能"神秘消失"
- 实战:WeakMap 实现私有变量封装
- Day 9 自检 Checklist
- 彩蛋:如何用 Chrome DevTools 看 WeakMap 条目回收
1. Map 与 WeakMap:表面区别 vs 内存差距
先看表格(有内涵的表格):
特性 | Map | WeakMap |
---|---|---|
键类型 | 任意类型(对象、字符串、Symbol) | 只能是对象 |
引用类型 | 强引用(阻止键对象被 GC) | 弱引用(不阻止键对象被 GC) |
可遍历性 | ✔ 支持 forEach 、keys() 等 |
❌ 不可遍历(因为 GC 随时可能清理条目) |
GC 行为 | 只要 Map 活着,键对象就活着 | 如果键对象无其他引用,GC 会清理 WeakMap 条目 |
🤔 那问题来了 :
为什么 WeakMap 不能遍历?因为 V8 不保证弱引用键一定存在,遍历会破坏 GC 设计。
2. 什么是弱引用?为什么 WeakMap 是弱引用?
强引用:
"你在对象身上挂了个标签:你不能死,你死了我也完蛋。"
普通 Map 的键就是强引用:
javascript
Global → Map → KeyObject → (堆)
GC 扫描时,发现 KeyObject 可达,就不会回收。
弱引用:
"你在对象身上写了个字条:你死了我不管,保重。"
WeakMap 对键是弱引用,GC 会忽略 WeakMap → 键的指针,如果对象没有其他强引用,直接回收。
⚠️ 关键区别:
- Map: 只要 Map 活着,键对象活到天荒地老。
- WeakMap: 如果外部没人引用键对象,GC 直接干掉它,WeakMap 条目同步消失。
3. V8 内存结构:堆、栈、隐藏的引用网络
栈(Stack)
- 存放执行上下文、基本类型、指针。
- 生命周期短,自动释放。
堆(Heap)
- 存放对象、闭包、函数、Map、WeakMap。
- V8 垃圾回收器(GC)负责回收不可达对象。
GC 的核心算法:可达性分析
- 从 根对象(全局对象、当前栈变量)出发,沿引用链标记所有能到达的对象。
- 没有被标记的,就是"垃圾"。
4. Map 和 WeakMap 在 V8 的底层实现
Map 用的就是普通哈希表,存储的是:
yaml
struct MapEntry {
Key: HeapObject* (强引用)
Value: HeapObject* (强引用)
}
WeakMap 则用了 Ephemeron Table(短命关联表) :
- 键:弱引用(GC 标记阶段会忽略它)。
- 值:强引用,但依附于键的生死。
V8 GC 扫描时:
- 如果键不可达 → 清理键和值。
- 如果键可达 → 值继续保留。
为什么 WeakMap 不可遍历?
因为 V8 不维护完整的引用图,条目随 GC 消失,遍历结果无法确定。
5. 闭包 + Map = 内存泄漏?WeakMap 如何破局
来看个典型问题:
ini
function createCache() {
const cache = new Map();
return function (obj, val) {
if (cache.has(obj)) return cache.get(obj);
cache.set(obj, val);
};
}
const save = createCache();
let user = { name: 'Dora' };
save(user, 'VIP');
// user = null; ❌ 即使置空,Map 仍引用 user
Map 强引用 user → GC 不能清理 → 内存泄漏。
改成 WeakMap:
ini
function createCache() {
const cache = new WeakMap();
return function (obj, val) {
if (cache.has(obj)) return cache.get(obj);
cache.set(obj, val);
};
}
let user = { name: 'Dora' };
const save = createCache();
save(user, 'VIP');
user = null; // ✅ GC 回收 user 和 WeakMap 条目
6. 柯里化与缓存实战
普通缓存(Map 版)
vbnet
function curry(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
问题:cache
持久化 → 永不释放。
WeakMap 缓存
php
function curryWithWeakMap(fn) {
const cache = new WeakMap();
return function (obj) {
if (cache.has(obj)) return cache.get(obj);
const result = fn(obj);
cache.set(obj, result);
return result;
};
}
✅ 对象没引用了,条目也没了,闭包安全 + 内存安全。
7. GC 工作原理:为什么 WeakMap 条目能"神秘消失"
V8 GC 在标记阶段:
-
遍历根 → 标记强引用对象。
-
遍历弱引用集合(ephemeron tables):
- 如果键未被标记 → 清理键和值。
因此,WeakMap 条目的生命周期完全取决于键对象的可达性。
简化伪代码:
scss
if (!isMarked(key)) {
clearEntry(key, value);
}
8. WeakMap 实现私有变量封装
javascript
const _private = new WeakMap();
class Person {
constructor(name) {
_private.set(this, { name });
}
getName() {
return _private.get(this).name;
}
}
let p = new Person('Dora');
console.log(p.getName()); // Dora
p = null; // ✅ 对象销毁,WeakMap 自动清理条目
9. Day 9 自检 Checklist
✔ 我能解释 Map 和 WeakMap 的区别,尤其是 GC 行为吗?
✔ 我知道什么是弱引用,为什么 GC 可以忽略它吗?
✔ 我理解 V8 的 ephemeron table 是如何实现 WeakMap 吗?
✔ 我能用 WeakMap 优化闭包缓存,防止内存泄漏吗?
✔ 我能在 Chrome DevTools 验证 WeakMap 条目回收吗?
🎁 彩蛋:用 Chrome 验证 WeakMap 条目回收
- 打开 Memory → Heap Snapshot。
- 创建 WeakMap 并插入对象。
- 清空对象引用 → 强制 GC → 查看条目是否消失。
🔥 一句话总结
Map 和 WeakMap 的区别,不只是"能不能 GC",而是 V8 背后的引用策略 + ephemeron table 机制 。理解它,才能写出性能安全的闭包缓存。