weakref仅避免阻止gc,不自动清理缓存条目;finalizationregistry才是感知对象真正回收并触发缓存键删除的唯一机制,二者需配合使用且不可替代。

WeakRef 本身不清理缓存,只让对象可被回收
很多人以为 WeakRef 一创建,缓存就“自动变轻”、会“自己消失”。不是这样。WeakRef 唯一作用是:不阻止目标对象被 GC。它自身是个普通对象,只要你还把它存进 Map 或 Object,这个 WeakRef 实例就会一直占内存;而一旦目标对象被回收,weakRef.deref() 就返回 undefined,但你的缓存结构里仍留着这个“空壳”。长期运行后,缓存表里堆满 undefined 占位符,查找变慢、内存反升。
所以关键判断是:WeakRef 只解决“能不能收走对象”,不解决“要不要删掉缓存条目”。后者必须你主动做。
FinalizationRegistry 是唯一能感知对象真正回收的机制
FinalizationRegistry 的回调不是“对象快死了来提醒你”,而是“对象已被 GC,此刻已不可访问,你只能做外部清理”。它和 WeakRef 是搭档关系,不是替代关系:
- 注册时传入的
heldValue(比如缓存 key)必须是string或symbol—— 普通对象作 key 会导致无法匹配、清理失败 - 回调函数里**绝对不能访问原对象**,因为此时它已经不存在了;只能做副作用,例如
cache.delete(key) - 必须在创建
WeakRef后立即调用registry.register(target, heldValue, unregisterToken),且unregisterToken要传WeakRef实例本身,否则后续无法精准注销(避免重复触发) - 回调执行时机不确定,可能延迟几秒甚至更久,不能用于实时性要求高的逻辑
构建资源池时必须拆开“存引用”和“清键值”两步
典型错误是把 registry.register() 放在 set() 外面、或复用同一个 registry 实例却没传对 unregisterToken,结果一个对象回收触发多次清理,或根本没触发。
正确做法是:每次 set(key, value) 时,同步完成三件事:
- 用
new WeakRef(value)包裹值 - 把
key → weakRef存入缓存Map - 立刻调用
registry.register(value, key, weakRef)—— 这里weakRef是注销凭证,确保将来能取消该注册(防止 key 冲突或重复注册)
示例片段:
const cache = new Map();
const registry = new FinalizationRegistry(key => {
cache.delete(key);
});
function setResource(key, obj) {
const ref = new WeakRef(obj);
cache.set(key, ref);
registry.register(obj, key, ref); // 必须传 ref 作为 token
}
资源池 get() 时必须检查 deref() 返回值并容忍 undefined
get(key) 不是直接返回缓存值,而是:
- 先取
weakRef = cache.get(key) - 再调
weakRef?.deref()—— 注意可选链,因为cache里可能已被registry清理,也可能还没清理但目标已回收 - 如果
deref()返回undefined,说明对象已失联,应视作缓存失效,而非报错或重试
不要在 get() 里尝试重建对象或 fallback 到强引用——这会破坏弱引用设计初衷,让内存泄漏卷土重来。真正的“自我清理”体现在:查不到就当它不存在,而不是拼命挽留。
最易被忽略的一点:FinalizationRegistry 的回调不会帮你处理“对象还在但业务上已过期”的情况。它只响应 GC,不响应业务生命周期。如果你的资源有 TTL、使用频次阈值等策略,得额外加定时轮询或 LRU 驱逐逻辑,和弱引用机制正交叠加。










