FinalizationRegistry

Baseline 广泛可用 *

此特性已得到良好支持,可在多种设备和浏览器版本上使用。自 2021 年 4 月起,所有浏览器均已支持此特性。

* 此特性的某些部分可能存在不同级别的支持。

FinalizationRegistry 对象允许您在值被垃圾回收时请求回调。

描述

FinalizationRegistry 提供了一种方式,可以在注册到注册表的值被回收(垃圾回收)后的某个时间点,请求调用一个清理回调。(清理回调有时也称为终结器。)

注意: 清理回调不应用于关键程序逻辑。有关详细信息,请参阅关于清理回调的注意事项

通过传入回调来创建注册表

js
const registry = new FinalizationRegistry((heldValue) => {
  // …
});

然后,通过调用 register 方法,传入值及其持有值,来注册您希望获得清理回调的任何值。

js
registry.register(target, "some value");

注册表不会保留对值的强引用,因为那样会适得其反(如果注册表强引用它,该值将永远不会被回收)。在 JavaScript 中,对象和未注册符号是可以被垃圾回收的,因此它们可以作为目标或令牌注册到 FinalizationRegistry 对象中。

如果 target 被回收,您的清理回调可能会在某个时间点被调用,并传入您为其提供的持有值(上面的示例中是 "some value")。持有值可以是您喜欢的任何值:原始值或对象,甚至是 undefined。如果持有值是对象,注册表会保留对它的引用(以便稍后将其传递给您的清理回调)。

如果您可能想稍后取消注册已注册的目标值,您可以传入第三个值,即您稍后调用注册表 unregister 函数以取消注册该值时将使用的取消注册令牌。注册表仅保留对取消注册令牌的弱引用。

通常使用目标值本身作为取消注册令牌,这完全没问题。

js
registry.register(target, "some value", target);
// …

// some time later, if you don't care about `target` anymore, unregister it
registry.unregister(target);

然而,它不必是相同的值;可以是不同的值。

js
registry.register(target, "some value", token);
// …

// some time later
registry.unregister(token);

尽可能避免使用

正确使用 FinalizationRegistry 需要仔细考虑,如果可能,最好避免使用。同样重要的是要避免依赖规范未保证的任何特定行为。垃圾回收何时、如何以及是否发生,取决于任何给定 JavaScript 引擎的实现。您在一个引擎中观察到的任何行为,在另一个引擎、同一引擎的另一个版本,甚至在同一版本引擎的稍有不同的情况下,都可能不同。垃圾回收是一个棘手的问题,JavaScript 引擎的实现者正在不断改进和优化他们的解决方案。

以下是引入 FinalizationRegistry提案中作者包含的一些具体要点:

垃圾回收器非常复杂。如果应用程序或库依赖于 GC 来及时、可预测地清理 WeakRef 或调用终结器[清理回调],它很可能会感到失望:清理可能比预期晚得多,或者根本不发生。造成这种不确定性的原因包括:

  • 即使两个对象同时变得不可达,一个对象也可能比另一个对象更早地被垃圾回收,例如,由于分代收集。
  • 垃圾回收工作可以使用增量和并发技术来分时完成。
  • 可以使用各种运行时启发式方法来平衡内存使用和响应能力。
  • JavaScript 引擎可能持有看起来似乎不可达的对象的引用(例如,在闭包或内联缓存中)。
  • 不同的 JavaScript 引擎可能以不同的方式执行这些操作,或者同一个引擎在其版本之间可能会更改其算法。
  • 复杂的因素可能导致对象被意外地长时间保留,例如与某些 API 一起使用。

关于清理回调的注意事项

  • 开发者不应依赖清理回调来执行关键程序逻辑。清理回调可能有助于在程序运行过程中减少内存使用,但除此之外可能不太有用。
  • 如果您刚刚将一个值注册到注册表,该目标将不会在当前 JavaScript 任务结束前被回收。有关详细信息,请参阅关于 WeakRefs 的注意事项
  • 符合标准的 JavaScript 实现,即使是进行垃圾回收的实现,也不要求调用清理回调。何时以及是否调用清理回调完全取决于 JavaScript 引擎的实现。当已注册对象被回收时,其相应的清理回调可能会在那时被调用,或者在之后某个时间调用,或者根本不调用。
  • 主要的实现很可能会在执行期间的某个时候调用清理回调,但这些调用可能比相关对象被回收的时间晚很多。此外,如果一个对象在两个注册表中注册,不能保证两个回调会相邻调用——一个可能被调用而另一个从未被调用,或者另一个可能晚得多才被调用。
  • 在某些情况下,即使是通常会调用清理回调的实现,也可能不太可能调用它们:
    • 当 JavaScript 程序完全关闭时(例如,在浏览器中关闭标签页)。
    • FinalizationRegistry 实例本身不再能被 JavaScript 代码访问时。
  • 如果 WeakRef 的目标也存在于 FinalizationRegistry 中,则 WeakRef 的目标会在与注册表关联的任何清理回调被调用之前或同时被清除;如果您的清理回调尝试对对象的 WeakRef 调用 deref,它将返回 undefined

构造函数

FinalizationRegistry()

创建一个新的 FinalizationRegistry 对象。

实例属性

这些属性定义在 FinalizationRegistry.prototype 上,并由所有 FinalizationRegistry 实例共享。

FinalizationRegistry.prototype.constructor

创建实例对象的构造函数。对于 FinalizationRegistry 实例,初始值是 FinalizationRegistry 构造函数。

FinalizationRegistry.prototype[Symbol.toStringTag]

[Symbol.toStringTag] 属性的初始值是字符串 "FinalizationRegistry"。该属性用于 Object.prototype.toString()

实例方法

FinalizationRegistry.prototype.register()

将对象注册到注册表,以便在对象被垃圾回收时/如果被垃圾回收时获得清理回调。

FinalizationRegistry.prototype.unregister()

将对象从注册表中取消注册。

示例

创建新注册表

通过传入回调来创建注册表

js
const registry = new FinalizationRegistry((heldValue) => {
  // …
});

注册对象以进行清理

然后,通过调用 register 方法,传入对象及其持有值,来注册您希望获得清理回调的任何对象。

js
registry.register(theObject, "some value");

回调永远不会同步调用

无论您对垃圾回收器施加多大的压力,清理回调都不会同步调用。对象可能会同步回收,但回调总会在当前任务完成后某个时间被调用。

js
let counter = 0;
const registry = new FinalizationRegistry(() => {
  console.log(`Array gets garbage collected at ${counter}`);
});

registry.register(["foo"]);

(function allocateMemory() {
  // Allocate 50000 functions — a lot of memory!
  Array.from({ length: 50000 }, () => () => {});
  if (counter > 5000) return;
  counter++;
  allocateMemory();
})();

console.log("Main job ends");
// Logs:
// Main job ends
// Array gets garbage collected at 5001

然而,如果您允许在每次分配之间有短暂的间歇,回调可能会更早被调用。

js
let arrayCollected = false;
let counter = 0;
const registry = new FinalizationRegistry(() => {
  console.log(`Array gets garbage collected at ${counter}`);
  arrayCollected = true;
});

registry.register(["foo"]);

(function allocateMemory() {
  // Allocate 50000 functions — a lot of memory!
  Array.from({ length: 50000 }, () => () => {});
  if (counter > 5000 || arrayCollected) return;
  counter++;
  // Use setTimeout to make each allocateMemory a different job
  setTimeout(allocateMemory);
})();

console.log("Main job ends");

不能保证回调会被更早调用,或者是否会被调用,但日志消息的计数器值可能小于 5000。

规范

规范
ECMAScript® 2026 语言规范
# sec-finalization-registry-objects

浏览器兼容性

另见