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

瀏覽器相容性

另見