JavaScript 拾遗: WeakRef 和 FinalizationRegistry

58 阅读3分钟

在 JavaScript 的内存管理世界里,WeakRefFinalizationRegistry 是两位“高级管家”。它们允许你处理对象的垃圾回收(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 就是用来在对象被回收后“收尸”的。

它让你注册一个回调函数,当某个对象被销毁时,这个回调会被触发。

核心用法

  1. 定义注册表: 指定清理逻辑。
  2. 注册对象: 将对象和你想传递给回调的“持有的值(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. 它们之间的协作关系

这两者经常配合使用,构建高效且不泄露内存的系统。

特性WeakRefFinalizationRegistry
关注点回收前(尝试访问可能消失的对象)回收后(执行善后工作)
主要方法.deref().register(), .unregister()
主要目的节省内存,避免缓存导致的内存泄漏资源清理,释放非 JS 内存资源

⚠️ 开发者必读:使用禁忌

虽然这两个工具很强大,但它们带有一种**“不确定性”**,使用时必须非常谨慎:

  1. 垃圾回收是不确定的: 你无法预测 GC 什么时候运行。即使你把引用设为 nullFinalizationRegistry 的回调可能在 1 秒后触发,也可能在 1 小时后触发,甚至永不触发。
  2. 不要在回调里写核心业务逻辑: 清理回调应该是辅助性的(如打日志、释放非内存资源)。永远不要依赖它来执行程序逻辑的关键步骤。
  3. 避免“对象复活”:FinalizationRegistry 的回调里,你拿不到原对象(它已经死了),你只能拿到注册时传入的 heldValue

一句话总结:

WeakRef 让你偷看垃圾桶里的东西还在不在,而 FinalizationRegistry 负责在垃圾车拖走东西后给你发个短信通知。