前端知库

WeakRef 和 FinalizationRegistry 是做什么的?

WeakRef 持有对象的弱引用,不阻止对象被回收,deref() 可能返回 undefined。FinalizationRegistry 在注册的对象被回收后可能调用清理回调,但回调不保证执行,也不保证时机。MDN 建议尽量避免使用,典型用途是可丢失的缓存。

参考答案

简答

做图片缓存时,希望内存紧张的时候缓存里的大对象能被回收,而不是因为缓存一直引用着就永远留着。WeakRef 就是为这种需求准备的:它引用一个对象,但不阻止这个对象被垃圾回收,需要时用 deref() 取出来,对象已经被回收就得到 undefined。

FinalizationRegistry 是配套的:把对象注册进去,对象被回收后,引擎可能会调用你给的清理回调,回调收到的是注册时给的“持有值”,比如缓存的键,可以用来删掉 Map 里已经失效的条目。

关键限制是不确定性。垃圾回收什么时候发生、会不会发生,都由引擎决定,deref() 可能一直不返回 undefined,清理回调也可能永远不执行。所以 MDN 说正确使用它们需要仔细考虑,能不用就不用,清理回调不能承担必需的程序逻辑。

MDN 举的典型用途是按 URL 缓存大对象的缓存系统。清理回调主要用来减少内存占用,比如删掉缓存里失效的条目;关闭连接、移除监听这类必须做的释放,仍然要在代码里显式完成。

基本用法

下面的例子能确定输出的只有“取回对象”和“取消注册”这部分,回收本身不能用代码触发。

JavaScript
let target = { name: '大对象' };
const ref = new WeakRef(target);
console.log(ref.deref() === target); // true
const registry = new FinalizationRegistry((heldValue) => {
  console.log('已回收', heldValue);
});
const token = {};
registry.register(target, 'cache-key', token);
console.log(registry.unregister(token)); // true
console.log(registry.unregister(token)); // false

为什么例子里没有演示“回收之后 deref() 返回 undefined”?因为即使把 target = null,什么时候回收也由引擎决定,可能马上,也可能一直不发生,写不出一个稳定的输出。MDN 还规定,刚创建 WeakRef 或刚通过 deref() 拿到对象后,这个对象在当前任务结束前不会被回收,所以同一段同步代码里多次 deref() 结果是一致的,不会一次拿到对象一次拿到 undefined。

register 的第三个参数是取消注册用的令牌。资源已经被主动释放时,调用 unregister(token) 取消回调,成功返回 true,再调一次就没有可取消的了,返回 false。注册表对令牌和目标对象都只是弱引用。持有值如果是对象,注册表会强引用它,所以持有值里不能再引用目标对象,否则目标永远不会被回收;直接把目标本身当持有值传入,register 会抛 TypeError。

写一个可回收的缓存

典型写法是用 Map 存 key → WeakRef。读缓存时先 deref(),拿到对象就用,拿到 undefined 就重新加载。再用 FinalizationRegistry 在对象回收后删掉对应的键,避免 Map 里堆满空的 WeakRef。删之前要再检查一次这个键当前存的 WeakRef 是否已经失效,因为同一个键可能已经放进了新对象。

这和 WeakMap 的用途不一样。WeakMap 是“对象作键”,对象没了条目自动消失,适合给对象附加数据;WeakRef 是“对象作值”,适合按字符串等键缓存对象。两者的对比见《Map、Set、WeakMap 与普通对象各有什么用途?》,内存回收的基本原理见《垃圾回收为什么无法自动解决所有内存泄漏?》。

为什么不推荐依赖它们

MDN 解释,规范刻意不保证回收行为,是为了不让垃圾回收的细节暴露给代码。否则大家会写依赖某个引擎回收时机的代码,引擎一改就坏了。不同引擎、同一引擎的不同版本、甚至同一页面在不同内存压力下,结果都可能不同。

MDN 还列出了通常不会调用回调的情况:程序整体退出(比如关闭标签页),或者 FinalizationRegistry 实例本身已经无法访问。所以清理回调只能做“做了更好,不做也不出错”的事,MDN 的原话是它可能有助于减少内存占用,此外不太有用。真正必须释放的资源,比如关闭连接、移除监听,要在业务流程里显式调用释放方法。

常见追问

清理回调里还能用 deref 拿到对象吗?

不能。MDN 说明,如果一个对象同时被 WeakRef 引用并注册在 FinalizationRegistry 里,WeakRef 的目标会在清理回调调用之前或同时被清空,回调里再 deref() 只能得到 undefined。回调里能用的只有注册时传的持有值。

能用它们检测内存泄漏吗?

只能做粗略的辅助观察,不能作为结论。回调不执行不代表对象泄漏了,可能只是引擎还没回收。排查泄漏应该用 DevTools 的堆快照,看是谁还在引用对象。