一、 从 Set/Map 到 WeakSet/WeakMap
在 JavaScript 中,Set 和 Map 是常用的集合类型:
- Set:类似于数组,但成员唯一。
- Map:键值对集合,键可以是任何类型。
它们都是强引用。只要集合实例存在,内部的对象就不会被垃圾回收(GC)。而 WeakSet/WeakMap 存储的是弱引用,专门用于解决内存泄漏问题。
二、 强引用与弱引用的博弈
- 强引用:像铁链。只要拴着,对象就无法被回收。
- 弱引用:像蛛丝。记录了对象位置,但 GC 清理内存时会直接忽略它。如果对象只剩下弱引用,就会被清理。
三、 内存消亡实验:揭开 GC 的面纱
实验准备
JavaScript
let tempObj = { name: 'Im still here' };
const ws = new WeakSet();
ws.add(tempObj);
tempObj = null; // 切断强引用
1. 实验:直接打印与手动回收
- 操作:点击 Chrome
Performance面板的“垃圾桶”图标。 - 现象:回收后打印,
ws变为空。
2. 实验:断点下的“顽固”对象
-
现象:断点时点一次回收,对象可能还在;连续点多次,对象消失。
-
原因:
- 分代回收:引擎可能只做了局部清理(Scavenge)。
- 非确定性:GC 取决于内存压力和引擎算法。
- 调试器干扰:断点状态可能产生隐藏强引用,需多次 Full GC 才能物理抹除。
四、 实际工程应用场景
1. 进度条(Loading)与按钮绑定
当业务要求点击按钮出现进度条,且进度条消失后其关联配置也应销毁时:
JavaScript
// 存储进度条的元数据(如开始时间、自定义配置)
const barConfigs = new WeakMap();
function createProgress(btn) {
const progressBar = document.createElement('div');
progressBar.className = 'progress-bar';
document.body.appendChild(progressBar);
// 绑定配置:Key 是 DOM 节点,Value 是配置对象
barConfigs.set(progressBar, {
startTime: Date.now(),
theme: 'dark'
});
// 模拟异步任务完成
setTimeout(() => {
const config = barConfigs.get(progressBar);
console.log(`耗时: ${Date.now() - config.startTime}ms`);
// 核心:移除 DOM 节点
progressBar.remove();
// 此时,progressBar 节点被销毁,barConfigs 里的记录也会被 GC 自动清理
}, 2000);
}
2. Dialog 对话框及其内容的自动化销毁
在 Dialog 中,我们经常挂载一些高内存对象(如 Echarts 实例或大型数据缓存)。
JavaScript
// 存储 Dialog 关联的复杂实例
const dialogRegistry = new WeakMap();
function openDialog() {
const dialog = document.createElement('div');
dialog.className = 'custom-dialog';
document.body.appendChild(dialog);
// 模拟一个占用内存的大型对象(如三方库实例)
const complexChart = {
instance: "EchartsInstance",
rawData: new Array(100000).fill('Heavy Data')
};
// 建立弱引用关联
dialogRegistry.set(dialog, { chart: complexChart });
// 点击遮罩层关闭
dialog.onclick = () => {
// 1. 获取关联数据进行清理动作(如注销监听)
const data = dialogRegistry.get(dialog);
console.log("清理实例:", data.chart.instance);
// 2. 物理删除 DOM
dialog.remove();
// 3. 重点:由于是 WeakMap,只要 dialog 节点消失,
// 它携带的 complexChart 大对象会自动进入回收名单
};
}
五、 总结
- Map/Set:用于持久、可遍历的业务数据存储。
- WeakMap/WeakSet:用于临时、随动、私有的辅助数据关联。
通过这种“生命周期同步”的写法,你不再需要手动编写大量的 obj = null 或是 map.delete(key),代码的健壮性和可维护性会显著提升。