在 JavaScript 的内存管理世界里,WeakRef 和 FinalizationRegistry 是两位“高级管家”。它们允许你处理对象的垃圾回收(GC)逻辑,而不会阻碍回收进程。
简单来说,它们是为了解决**“既想追踪对象,又不希望对象因为我的追踪而死活不掉”**的问题。
1. WeakRef (弱引用)
通常我们创建一个引用(比如 const a = { name: 'Gemini' }),只要 a 还在,对象就不会被回收。这叫强引用。
WeakRef 允许你持有一个对象的弱引用。如果一个对象只剩弱引用指向它,垃圾回收机制(GC)照样会把它收走。
核心用法
- 创建:
new WeakRef(targetObject) - 读取:
.deref()。如果对象还没被回收,返回该对象;如果已被回收,返回undefined。
JavaScript
let user = { name: "Alice" };
const weakUser = new WeakRef(user);
// 只要 user 没被回收
console.log(weakUser.deref().name); // "Alice"
// 手动断开强引用
user = null;
// 在未来的某个时刻,GC 运行后:
// weakUser.deref() 会变成 undefined
使用场景
- 缓存系统: 缓存大型对象(如图像或数据表),当内存紧张且没有其他地方使用这些对象时,允许 GC 自动清理它们。
2. FinalizationRegistry (清理注册表)
如果说 WeakRef 是用来“看”对象还在不在,那么 FinalizationRegistry 就是用来在对象被回收后“收尸”的。
它让你注册一个回调函数,当某个对象被销毁时,这个回调会被触发。
核心用法
- 定义注册表: 指定清理逻辑。
- 注册对象: 将对象和你想传递给回调的“持有的值(heldValue)”绑定。
JavaScript
const registry = new FinalizationRegistry((heldValue) => {
console.log(`对象已被回收,清理标记为: ${heldValue}`);
});
let obj = { data: "important" };
// 注册 obj,当它被回收时,回调函数会收到 "some_id"
registry.register(obj, "some_id");
// 断开引用,等待 GC
obj = null;
使用场景
- 外部资源释放: 当 JS 对象被回收时,自动关闭与之关联的 WebSocket 连接、释放 C++ 层面的文件描述符或清理 DOM 节点。
3. 它们之间的协作关系
这两者经常配合使用,构建高效且不泄露内存的系统。
| 特性 | WeakRef | FinalizationRegistry |
|---|---|---|
| 关注点 | 回收前(尝试访问可能消失的对象) | 回收后(执行善后工作) |
| 主要方法 | .deref() | .register(), .unregister() |
| 主要目的 | 节省内存,避免缓存导致的内存泄漏 | 资源清理,释放非 JS 内存资源 |
⚠️ 开发者必读:使用禁忌
虽然这两个工具很强大,但它们带有一种**“不确定性”**,使用时必须非常谨慎:
- 垃圾回收是不确定的: 你无法预测 GC 什么时候运行。即使你把引用设为
null,FinalizationRegistry的回调可能在 1 秒后触发,也可能在 1 小时后触发,甚至永不触发。 - 不要在回调里写核心业务逻辑: 清理回调应该是辅助性的(如打日志、释放非内存资源)。永远不要依赖它来执行程序逻辑的关键步骤。
- 避免“对象复活”: 在
FinalizationRegistry的回调里,你拿不到原对象(它已经死了),你只能拿到注册时传入的heldValue。
一句话总结:
WeakRef让你偷看垃圾桶里的东西还在不在,而FinalizationRegistry负责在垃圾车拖走东西后给你发个短信通知。