这篇文章发布于 2026年04月13日,星期一,16:01,归类于 JS API。 阅读 92 次, 今日 91 次 没有评论
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12155
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。
一、weak三剑客
JS语言中的弱类型除了本文要介绍的WeakRef,还有WeakMap和WeakSet,其中WeakMap我5年前有介绍过,参见“JS WeakMap应该什么时候使用”。
WeakMap和WeakSet支持很多很多年了,无需顾忌任何兼容性问题,但是WeakRef则是前几年刚支持的特性。

三者的本质区别
用一句话说明WeakMap、WeakSet和WeakRef的区别,那就是:
WeakMap 和 WeakSet 是集合类数据结构,弱引用是它们管理成员的方式;WeakRef 是对单个对象的弱引用包装器,让你能主动检查对象是否还活着。
用更加通俗的话解释就是:
- WeakMap:我想给对象贴标签/存数据,但不阻止它被回收
- WeakSet:我想记住哪些对象出现过,但不阻止它们被回收
- WeakRef:我想保留一个对象的引用,但允许它随时被回收,用时再检查
下面案例时刻:
1. WeakMap —— 以对象为键的键值映射
const wm = new WeakMap();
let obj = { name: 'Alice' };
wm.set(obj, '额外数据'); // 键必须是对象
console.log(wm.get(obj)); // '额外数据'
obj = null; // obj 被回收后,WeakMap 中对应的条目也会自动消失
补充说明:
- 键必须是对象(不能是原始值)
- 值可以是任意类型
- 当键对象没有其他引用时,键值对会被 GC 自动回收
- 常用于:DOM 节点关联数据、类的私有数据存储
2. WeakSet —— 对象的弱引用集合
const ws = new WeakSet();
let obj = { id: 1 };
ws.add(obj); // 只能添加对象
console.log(ws.has(obj)); // true
obj = null; // obj 被回收后,WeakSet 中的条目也自动消失
补充说明:
- 只能存储对象,不能存原始值
- 只有
add、has、delete三个方法 - 常用于:标记对象是否”已访问”、”已处理”、防止循环引用
3. WeakRef —— 对单个对象的弱引用
let obj = { data: 'heavy resource' };
const ref = new WeakRef(obj);
// 通过 .deref() 获取目标对象
console.log(ref.deref()); // { data: 'heavy resource' }
console.log(ref.deref()?.data); // 'heavy resource'
obj = null;
// 某次 GC 之后...
console.log(ref.deref()); // undefined(对象已被回收)
要点说明:
- 包装单个对象,不是集合
- 通过
.deref()取回对象,如果已被 GC 回收则返回undefined - 通常搭配
FinalizationRegistry使用,在对象被回收时执行清理回调 - 常用于:缓存(对象在则命中缓存,被回收则重新创建)
二、JS WeakRef 弱引用经典案例
列举几个适合使用JS WeakRef的场景,希望可以让大家对WeakRef的作用有更加深刻的理解。
1. 缓存场景
const cache = new Map();
function getCached(key, createFn) {
const ref = cache.get(key);
const cached = ref?.deref();
if (cached) return cached; // 缓存命中
// 缓存未命中或已被 GC 回收,重新创建
const newObj = createFn();
cache.set(key, new WeakRef(newObj));
return newObj;
}
这里 WeakRef 允许缓存的大对象在内存紧张时被自动回收,避免内存泄漏,个人观点,缓存处理是WeakRef最具代表性的应用场景。
2. DOM 元素不泄漏的临时引用
先看DOM元素删除,但是内存依然占用的例子:
// 典型泄漏场景:DOM 节点已从页面移除,但 JS 仍持有强引用
const detachedNodes = [];
function addItem() {
const div = document.createElement('div');
document.body.appendChild(div);
detachedNodes.push(div); // 强引用
}
function removeItem(div) {
document.body.removeChild(div);
// 虽然从 DOM 树移除了,但 detachedNodes 数组还引用着它
// → GC 无法回收 → 内存泄漏!
}
WeakRef可以解决这个问题:
const nodeRefs = [];
function addItem() {
const div = document.createElement('div');
document.body.appendChild(div);
nodeRefs.push(new WeakRef(div)); // 弱引用,不阻止 GC
}
function doSomethingWithNodes() {
for (const ref of nodeRefs) {
const node = ref.deref();
if (node) {
// 节点还活着,正常操作
node.style.color = 'red';
} else {
// 节点已被 GC 回收,跳过
}
}
}
当 DOM 节点从文档中移除且没有其他强引用时,WeakRef 不会阻止 GC 回收它。
但是,虽然WeakRef有避免DOM内存泄露的能力,但我个人觉得不推荐这么使用,原因后面会解释。
3. 事件监听 + 不阻止 GC
有内存问题的代码示意:
// ❌ 泄漏:闭包持有 DOM 引用,且监听器未清理
function setup() {
const el = document.getElementById('target');
window.addEventListener('resize', () => {
el.style.width = '100%'; // el 被闭包捕获,永远无法回收
});
}
如果使用WeakRef:
function setup() {
const ref = new WeakRef(document.getElementById('target'));
window.addEventListener('resize', () => {
const el = ref.deref();
if (!el) return; // 对象没了,直接跳过
el.style.width = '100%';
});
}
还是那句话,这里其实使用AbortController主动移出监听器是更好的做法。
4. 组件实例池 / 对象池(复用但不泄漏)
比方说下面这段代码,对象销毁后自动从池子里 “消失”,不用手动清理。
const pool = new Set();
function addToPool(obj) {
pool.add(new WeakRef(obj));
}
function foreachActive(cb) {
for (const ref of pool) {
const obj = ref.deref();
if (obj) cb(obj);
else pool.delete(ref);
}
}
5. 防止循环引用导致的内存泄漏
这个案例中父 ↔ 子互相引用,但是由于使用了WeakRef,打破了强引用链,GC 能正常工作,内存不会泄露。
class Parent {
constructor() {
this.child = null;
}
}
class Child {
constructor(parent) {
this.parentRef = new WeakRef(parent); // 弱引用
}
}
const p = new Parent();
const c = new Child(p);
p.child = c;
OK,虽然上面展示了很多可以使用WeakRef的场景,但是这些处理手段都不推荐(除了第一个场景),只能作为避免内存泄漏的最后手段。
三、谨慎使用WeakRef
为什么要谨慎使用WeakRef?最重要的原因其实也就四个字——“不可预测”!
垃圾收集何时、如何以及是否发生,取决于任何给定JavaScript引擎的实现。你在一个引擎中观察到的任何行为,在另一个引擎中、在同一引擎的另一个版本中,甚至在相同引擎的相同版本但在稍有不同的情况下,都可能会有所不同。
比方说某个内存,你明明想要使用,但是却被回收了,这就很烦人。
我之前开发就遇到过,大数据量的视频buffer数据,最终视频合成的时候莫名丢失了,使得我不得不一开始的时候强引用,当时我也是没有搞懂浏览器引擎的回收机制,根据查阅资料的说法,就是内存不足的时候,某些未被强引用的巨大变量会被回收,即使这个变量最后需要使用。
又比如说,你一位内存已经被清理了,可实际上内存清理工作可能会比预期晚得多才进行,或者根本不会进行。
因为JavaScript 引擎的内存回收机制极为复杂:
- 即使两个对象同时变得无法访问(例如,由于分代收集),一个对象可能比另一个对象更早地被垃圾收集。
- 垃圾收集工作可以通过使用增量和并发技术来分时段进行。
- 可以使用各种运行时启发式算法来平衡内存使用和响应性。
- JavaScript 引擎可能会保留对看似无法访问的内容的引用(例如,在闭包或内联缓存中)。
- 不同的JavaScript引擎处理这些事情的方式可能不同,或者同一个引擎在不同版本中可能会改变其算法。
- 复杂的因素可能导致对象被意外地保留很长时间,例如与某些应用程序编程接口(API)一起使用时。
所以,在绝大多数场景下,主动清理引用(移除监听器、清空变量、useEffect return cleanup)才是正道。
WeakRef 的 deref() 返回 undefined 的时机依赖 GC,不可预测,所以不应该用它来做确定性的资源管理。
例如上面案例提到的事件管理,下面的AbortController方法是更加推荐的:
function setup() {
const el = document.getElementById('target');
const controller = new AbortController();
window.addEventListener('resize', () => {
el.style.width = '100%';
}, { signal: controller.signal });
// 需要清理时
controller.abort(); // 自动移除监听器
}
我个人的观点是这样的:你该怎么开发就怎么开发,不用管什么内存没有回收这些,大多数场景下,你多使用点内存,少一点内存,对用户影响几乎没有,让JS引擎自己去维护吧,除非遇到内存占有很大,不得不去处理的场景,这个时候,大家再去考虑使用WeakRef做内存优化。
四、结语碎碎念
我的个人博客开始出现很多AI味十足的评论,先是一句话总结文章内容,然后对内容和作者无脑吹,什么“硬核”,什么“高大上”。
还不如以前回复的“写的狗屁不通”这样的评论。[叹气]
最近刷漫剧,发现下面也有很多这样的评论,都是AI机器人。
其实龙虾火了之后,这样的东西会越来越普遍。
比方说即有同事弄了自动去社交网站评论的龙虾。
不过我目前还未深入这类AI工具,不急不急,对我而言,掌握更基础的知识与技能,比追逐流行工具更有价值。
好了,就说这么多吧,感谢大家的阅读,我们下周再见!

😉😊😇
🥰😍😘
本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=12155
(本篇完)
- 翻译-js表达式闭包(expression closures)的进一步亲密接触 (0.244)
- 使用AbortController abort中断原生fetch或axios请求 (0.244)
- 实用的JS对象分组静态方法Object.groupBy() (0.223)
- JS WeakMap应该什么时候使用 (0.163)
- 漫谈js自定义事件、DOM/伪DOM自定义事件 (0.122)
- 小tips: 点击页面出现富强、民主这类文字动画效果 (0.122)
- JS CustomEvent自定义事件传参小技巧 (0.122)
- 翻译 - CSS Sprites:实用技术还是生厌之物? (0.098)
- 翻译:清除各个浏览器中的数据研究 (0.098)
- 翻译:web制作、开发人员需知的Web缓存知识 (0.098)
- JS改变AudioBuffer音量并下载为新audio音频 (RANDOM - 0.070)