この記事では、非常に狭い範囲のトピックを取り上げています。これは、ほとんどの開発者が実際には非常にめったに出くわすことがない(存在すら知らない可能性のある)トピックです。
JavaScriptの学習を始めたばかりの場合は、この章をスキップすることをお勧めします。
ガベージコレクションの章から到達可能性原則の基本的な概念を思い出してみると、JavaScriptエンジンは、アクセス可能または使用中の値をメモリに保持することが保証されていることがわかります。
例えば
// the user variable holds a strong reference to the object
let user = { name: "John" };
// let's overwrite the value of the user variable
user = null;
// the reference is lost and the object will be deleted from memory
または、2つの強い参照を持つ、少し複雑なコード。
// the user variable holds a strong reference to the object
let user = { name: "John" };
// copied the strong reference to the object into the admin variable
let admin = user;
// let's overwrite the value of the user variable
user = null;
// the object is still reachable through the admin variable
オブジェクト{ name: "John" }
は、強い参照がなくなっている場合(admin
変数の値を上書きした場合も)のみ、メモリから削除されます。
JavaScriptでは、この場合、少し異なる動作をするWeakRef
と呼ばれる概念があります。
強い参照 – オブジェクトまたは値への参照であり、ガベージコレクタによる削除を防ぎます。これにより、オブジェクトまたは値が指しているメモリが保持されます。
これは、アクティブな強い参照が存在する限り、オブジェクトまたは値はメモリに残っており、ガベージコレクタによって収集されないことを意味します。
JavaScriptでは、オブジェクトへの通常の参照は強い参照です。例えば
// the user variable holds a strong reference to this object
let user = { name: "John" };
弱い参照 – オブジェクトまたは値への参照であり、ガベージコレクタによる削除を防ぎません。オブジェクトまたは値は、それらへの残りの参照が弱い参照のみである場合、ガベージコレクタによって削除される可能性があります。
WeakRef
詳しく説明する前に、この記事で説明されている構造の正しい使用は、非常に慎重な検討が必要であり、可能であれば避けるのが最善であることに注意する価値があります。
WeakRef
– target
またはreferent
と呼ばれる別のオブジェクトへの弱い参照を含むオブジェクトです。
WeakRef
の特殊性は、ガベージコレクタが参照オブジェクトを削除するのを防がないことです。言い換えれば、WeakRef
オブジェクトはreferent
オブジェクトを存続させません。
ここで、user
変数を「referent」として使用し、そこからadmin
変数への弱い参照を作成します。弱い参照を作成するには、WeakRef
コンストラクタを使用し、ターゲットオブジェクト(弱い参照が必要なオブジェクト)を渡す必要があります。
私たちの場合、これはuser
変数です。
// the user variable holds a strong reference to the object
let user = { name: "John" };
// the admin variable holds a weak reference to the object
let admin = new WeakRef(user);
以下の図は、user
変数を使用する強い参照とadmin
変数を使用する弱い参照の2種類の参照を示しています。
その後、ある時点でuser
変数の使用を停止します(上書きされ、スコープ外になりますなど)。ただし、admin
変数にWeakRef
インスタンスを保持します。
// let's overwrite the value of the user variable
user = null;
オブジェクトへの弱い参照だけでは、オブジェクトを「存続」させるには不十分です。参照オブジェクトへの残りの参照が弱い参照のみである場合、ガベージコレクタは自由にこのオブジェクトを破棄し、そのメモリを他の用途に使用できます。
ただし、オブジェクトが実際に破棄されるまで、弱い参照は、このオブジェクトへの強い参照がなくなっている場合でも、オブジェクトを返す可能性があります。つまり、私たちのオブジェクトは一種の「シュレディンガーの猫」になります。「生きている」か「死んでいる」かを確実に知ることはできません。
この時点で、WeakRef
インスタンスからオブジェクトを取得するには、そのderef()
メソッドを使用します。
deref()
メソッドは、オブジェクトがまだメモリにある場合、WeakRef
が指す参照オブジェクトを返します。オブジェクトがガベージコレクタによって削除されている場合、deref()
メソッドはundefined
を返します。
let ref = admin.deref();
if (ref) {
// the object is still accessible: we can perform any manipulations with it
} else {
// the object has been collected by the garbage collector
}
WeakRefのユースケース
WeakRef
は、通常、リソースを大量に消費するオブジェクトを格納するキャッシュまたは連想配列を作成するために使用されます。これにより、キャッシュまたは連想配列に存在するだけで、これらのオブジェクトがガベージコレクタによって収集されるのを防ぐことができます。
主要な例の一つとして、多数のバイナリイメージオブジェクト(例えば、ArrayBuffer
またはBlob
として表される)があり、各イメージに名前またはパスを関連付ける必要がある状況があります。既存のデータ構造は、これらの目的にはあまり適していません。
Map
を使用して名前とイメージの間に関連付けを作成すると、キーまたは値としてMap
に存在するため、イメージオブジェクトはメモリに保持されます。WeakMap
もこの目的には適していません。WeakMap
のキーとして表されるオブジェクトは弱い参照を使用し、ガベージコレクタによる削除から保護されないためです。
しかし、この状況では、値に弱い参照を使用するデータ構造が必要です。
この目的のために、値が必要な大きなオブジェクトを参照するWeakRef
インスタンスであるMap
コレクションを使用できます。その結果、これらの大きく不要なオブジェクトは、必要以上にメモリに保持されません。
そうでなければ、これは、アクセス可能であればキャッシュからイメージオブジェクトを取得する方法です。ガベージコレクションされている場合は、再度生成またはダウンロードします。
このようにして、状況によってはメモリ使用量が少なくなります。
例№1:キャッシングのためのWeakRefの使用
以下は、WeakRef
の使用テクニックを示すコードスニペットです。
簡単に言うと、文字列キーと値としてWeakRef
オブジェクトを持つMap
を使用します。WeakRef
オブジェクトがガベージコレクタによって削除されていない場合、キャッシュから取得します。そうでない場合は、再度ダウンロードして、さらに再利用するためにキャッシュに入れます。
function fetchImg() {
// abstract function for downloading images...
}
function weakRefCache(fetchImg) { // (1)
const imgCache = new Map(); // (2)
return (imgName) => { // (3)
const cachedImg = imgCache.get(imgName); // (4)
if (cachedImg?.deref()) { // (5)
return cachedImg?.deref();
}
const newImg = fetchImg(imgName); // (6)
imgCache.set(imgName, new WeakRef(newImg)); // (7)
return newImg;
};
}
const getCachedImg = weakRefCache(fetchImg);
ここで何が起こったのかを詳しく見てみましょう。
weakRefCache
– 別の関数fetchImg
を引数として取る高階関数です。この例では、fetchImg
関数はイメージをダウンロードするための任意のロジックであるため、詳細な説明は省略できます。imgCache
–fetchImg
関数のキャッシュされた結果を、文字列キー(イメージ名)と値としてWeakRef
オブジェクトの形式で格納するイメージのキャッシュです。- イメージ名を引数として取る無名関数を返します。この引数は、キャッシュされたイメージのキーとして使用されます。
- 指定されたキー(イメージ名)を使用して、キャッシュからキャッシュされた結果を取得しようとします。
- キャッシュに指定されたキーの値が含まれており、
WeakRef
オブジェクトがガベージコレクタによって削除されていない場合は、キャッシュされた結果を返します。 - 要求されたキーを持つキャッシュにエントリがない場合、または
deref()
メソッドがundefined
を返す場合(WeakRef
オブジェクトがガベージコレクションされたことを意味します)、fetchImg
関数はイメージを再度ダウンロードします。 - ダウンロードされたイメージを
WeakRef
オブジェクトとしてキャッシュに入れます。
これで、キーが文字列としてイメージ名であり、値がイメージ自体を含むWeakRef
オブジェクトであるMap
コレクションができました。
このテクニックは、もはや誰も使用していないリソースを大量に消費するオブジェクトに対して大量のメモリを割り当てるのを避けるのに役立ちます。また、キャッシュされたオブジェクトを再利用する場合にも、メモリと時間を節約できます。
このコードがどのように見えるかの視覚的な表現を以下に示します。
しかし、この実装には欠点があります。時間の経過とともに、Map
は、参照オブジェクトが既にガベージコレクションされたWeakRef
を指す文字列キーでいっぱいになります。
この問題に対処する1つの方法は、定期的にキャッシュをスカベンジングして「死んだ」エントリをクリアすることです。もう1つの方法は、次に説明するファイナライザを使用することです。
例№2:DOMオブジェクトの追跡のためのWeakRefの使用
WeakRef
のもう一つのユースケースは、DOMオブジェクトの追跡です。
サードパーティのコードまたはライブラリが、DOMに存在する限り、ページ上の要素と対話するというシナリオを考えてみましょう。たとえば、システムの状態を監視および通知するための外部ユーティリティ(一般的に「ロガー」と呼ばれ、情報を送信するプログラム。「ログ」と呼ばれるメッセージ)です。
インタラクティブな例
const startMessagesBtn = document.querySelector('.start-messages'); // (1)
const closeWindowBtn = document.querySelector('.window__button'); // (2)
const windowElementRef = new WeakRef(document.querySelector(".window__body")); // (3)
startMessagesBtn.addEventListener('click', () => { // (4)
startMessages(windowElementRef);
startMessagesBtn.disabled = true;
});
closeWindowBtn.addEventListener('click', () => document.querySelector(".window__body").remove()); // (5)
const startMessages = (element) => {
const timerId = setInterval(() => { // (6)
if (element.deref()) { // (7)
const payload = document.createElement("p");
payload.textContent = `Message: System status OK: ${new Date().toLocaleTimeString()}`;
element.deref().append(payload);
} else { // (8)
alert("The element has been deleted."); // (9)
clearInterval(timerId);
}
}, 1000);
};
.app {
display: flex;
flex-direction: column;
gap: 16px;
}
.start-messages {
width: fit-content;
}
.window {
width: 100%;
border: 2px solid #464154;
overflow: hidden;
}
.window__header {
position: sticky;
padding: 8px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #736e7e;
}
.window__title {
margin: 0;
font-size: 24px;
font-weight: 700;
color: white;
letter-spacing: 1px;
}
.window__button {
padding: 4px;
background: #4f495c;
outline: none;
border: 2px solid #464154;
color: white;
font-size: 16px;
cursor: pointer;
}
.window__body {
height: 250px;
padding: 16px;
overflow: scroll;
background-color: #736e7e33;
}
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="index.css">
<title>WeakRef DOM Logger</title>
</head>
<body>
<div class="app">
<button class="start-messages">Start sending messages</button>
<div class="window">
<div class="window__header">
<p class="window__title">Messages:</p>
<button class="window__button">Close</button>
</div>
<div class="window__body">
No messages.
</div>
</div>
</div>
<script type="module" src="index.js"></script>
</body>
</html>
「メッセージの送信を開始」ボタンをクリックすると、いわゆる「ログ表示ウィンドウ」(.window__body
クラスの要素)にメッセージ(ログ)が表示され始めます。
しかし、この要素がDOMから削除されるとすぐに、ロガーはメッセージの送信を停止する必要があります。この要素の削除を再現するには、右上の「閉じる」ボタンをクリックするだけです。
作業を複雑にせず、DOM要素が利用可能な場合と利用できない場合にサードパーティのコードに毎回通知しないようにするために、WeakRef
を使用して弱い参照を作成するだけで十分です。
要素がDOMから削除されると、ロガーはそのことに気づき、メッセージの送信を停止します。
では、ソースコード(タブindex.js
)を詳しく見てみましょう。
-
「メッセージの送信を開始」ボタンのDOM要素を取得します。
-
「閉じる」ボタンのDOM要素を取得します。
-
new WeakRef()
コンストラクタを使用してログ表示ウィンドウのDOM要素を取得します。このようにして、windowElementRef
変数はDOM要素への弱い参照を保持します。 -
クリックされたときにロガーを開始する役割を担う、「メッセージの送信を開始」ボタンにイベントリスナーを追加します。
-
クリックされたときにログ表示ウィンドウを閉じる役割を担う、「閉じる」ボタンにイベントリスナーを追加します。
-
setInterval
を使用して、1秒ごとに新しいメッセージを表示し始めます。 -
ログ表示ウィンドウのDOM要素がまだアクセス可能であり、メモリに保持されている場合、新しいメッセージを作成して送信します。
-
deref()
メソッドがundefined
を返す場合、DOM要素がメモリから削除されたことを意味します。この場合、ロガーはメッセージの表示を停止し、タイマーをクリアします。 -
ログ表示ウィンドウのDOM要素がメモリから削除された後(つまり、「閉じる」ボタンをクリックした後)に呼び出される
alert
です。メモリからの削除は、ガベージコレクタの内部メカニズムに依存するため、すぐに起こるとは限りません。このプロセスはコードから直接制御できません。しかし、それにもかかわらず、ブラウザからガベージコレクションを強制的に実行するオプションがあります。
たとえば、Google Chromeでは、開発者ツールを開く必要があります(Windows/LinuxではCtrl + Shift + J、macOSではOption + ⌘ + J)。「パフォーマンス」タブに移動し、ゴミ箱アイコンボタン「ガベージコレクション」をクリックします。
この機能は、ほとんどの最新のブラウザでサポートされています。操作を実行した後、alert
がすぐにトリガーされます。
FinalizationRegistry
ファイナライザについて説明します。先に進む前に、用語を明確にしておきましょう。
クリーンアップコールバック(ファイナライザ) - FinalizationRegistry
に登録されたオブジェクトがガベージコレクタによってメモリから削除されたときに実行される関数です。
その目的は、オブジェクトがメモリから完全に削除された後、オブジェクトに関連する追加の操作を実行できるようにすることです。
レジストリ(またはFinalizationRegistry
)- オブジェクトとそのクリーンアップコールバックの登録と登録解除を管理するJavaScriptの特殊なオブジェクトです。
このメカニズムにより、オブジェクトを登録してクリーンアップコールバックを関連付けることができます。基本的に、登録されたオブジェクトとそのクリーンアップコールバックに関する情報を保存し、オブジェクトがメモリから削除されたときにそれらのコールバックを自動的に呼び出す構造です。
FinalizationRegistry
のインスタンスを作成するには、コンストラクタを呼び出す必要があります。コンストラクタは、クリーンアップコールバック(ファイナライザ)を単一の引数として受け取ります。
構文
function cleanupCallback(heldValue) {
// cleanup callback code
}
const registry = new FinalizationRegistry(cleanupCallback);
ここで
cleanupCallback
- 登録されたオブジェクトがメモリから削除されたときに自動的に呼び出されるクリーンアップコールバックです。heldValue
- クリーンアップコールバックに引数として渡される値です。heldValue
がオブジェクトの場合、レジストリはそのオブジェクトへの強い参照を保持します。registry
-FinalizationRegistry
のインスタンスです。
FinalizationRegistry
メソッド
-
register(target, heldValue [, unregisterToken])
- レジストリにオブジェクトを登録するために使用されます。target
- 追跡のために登録されるオブジェクトです。target
がガベージコレクションされると、クリーンアップコールバックがheldValue
を引数として呼び出されます。オプションの
unregisterToken
- 登録解除トークンです。ガベージコレクタがオブジェクトを削除する前に、オブジェクトの登録を解除するために渡すことができます。通常、target
オブジェクトがunregisterToken
として使用され、これが標準的な方法です。 -
unregister(unregisterToken)
-unregister
メソッドは、レジストリからオブジェクトの登録を解除するために使用されます。1つの引数、unregisterToken
(オブジェクトの登録時に取得した登録解除トークン)を受け取ります。
では、簡単な例に進みましょう。既に知っているuser
オブジェクトを使用し、FinalizationRegistry
のインスタンスを作成します。
let user = { name: "John" };
const registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue} has been collected by the garbage collector.`);
});
次に、register
メソッドを呼び出すことで、クリーンアップコールバックが必要なオブジェクトを登録します。
registry.register(user, user.name);
レジストリは、登録されたオブジェクトへの強い参照を保持しません。これは、レジストリの目的を無効にするためです。レジストリが強い参照を保持すると、オブジェクトはガベージコレクションされません。
オブジェクトがガベージコレクタによって削除されると、将来のある時点で、heldValue
が渡されてクリーンアップコールバックが呼び出される可能性があります。
// When the user object is deleted by the garbage collector, the following message will be printed in the console:
"John has been collected by the garbage collector."
クリーンアップコールバックを使用する実装でも、呼び出されない可能性がある状況があります。
例えば
- プログラムが動作を完全に終了した場合(たとえば、ブラウザでタブを閉じた場合)。
FinalizationRegistry
インスタンス自体がJavaScriptコードからアクセスできなくなった場合。FinalizationRegistry
インスタンスを作成するオブジェクトがスコープ外になったり削除されたりすると、そのレジストリに登録されたクリーンアップコールバックも呼び出されない可能性があります。
FinalizationRegistryを使用したキャッシング
弱いキャッシュの例に戻ると、次のことに気付きます。
WeakRef
でラップされた値がガベージコレクタによって収集された場合でも、「メモリリーク」の問題が、値がガベージコレクタによって収集されたキーとして残っています。
FinalizationRegistry
を使用した、改善されたキャッシングの例を次に示します。
function fetchImg() {
// abstract function for downloading images...
}
function weakRefCache(fetchImg) {
const imgCache = new Map();
const registry = new FinalizationRegistry((imgName) => { // (1)
const cachedImg = imgCache.get(imgName);
if (cachedImg && !cachedImg.deref()) imgCache.delete(imgName);
});
return (imgName) => {
const cachedImg = imgCache.get(imgName);
if (cachedImg?.deref()) {
return cachedImg?.deref();
}
const newImg = fetchImg(imgName);
imgCache.set(imgName, new WeakRef(newImg));
registry.register(newImg, imgName); // (2)
return newImg;
};
}
const getCachedImg = weakRefCache(fetchImg);
-
関連付けられた
WeakRef
オブジェクトがガベージコレクタによって収集された場合に、「死んだ」キャッシュエントリのクリーンアップを管理するために、FinalizationRegistry
クリーンアップレジストリを作成します。重要な点は、クリーンアップコールバックで、エントリがガベージコレクタによって削除され、再追加されていないかどうかを確認する必要があることです。「ライブ」エントリを削除しないためです。
-
新しい値(画像)をダウンロードしてキャッシュに格納したら、
WeakRef
オブジェクトを追跡するために、ファイナライザレジストリに登録します。
この実装には、実際のものまたは「ライブ」のキー/値のペアのみが含まれています。この場合、各WeakRef
オブジェクトはFinalizationRegistry
に登録されます。そして、オブジェクトがガベージコレクタによってクリーンアップされると、クリーンアップコールバックはすべてのundefined
値を削除します。
更新されたコードの視覚的な表現を次に示します。
更新された実装の重要な側面は、ファイナライザにより、「メイン」プログラムとクリーンアップコールバックの間に並列プロセスを作成できることです。JavaScriptのコンテキストでは、「メイン」プログラムとは、アプリケーションまたはWebページで実行されるJavaScriptコードです。
したがって、オブジェクトがガベージコレクタによって削除対象としてマークされた時点から、クリーンアップコールバックの実際の実行まで、ある程度の時間差がある可能性があります。この時間差の間、メインプログラムはオブジェクトに変更を加えたり、メモリに戻したりすることもできることに注意することが重要です。
そのため、クリーンアップコールバックでは、「ライブ」エントリを削除しないように、エントリがメインプログラムによってキャッシュに追加されていないかどうかを確認する必要があります。同様に、キャッシュ内でキーを検索する場合、値がガベージコレクタによって削除されている可能性がありますが、クリーンアップコールバックはまだ実行されていません。
このような状況は、FinalizationRegistry
を使用する場合、特に注意が必要です。
実践におけるWeakRefとFinalizationRegistryの使用
理論から実践に移り、ユーザーがモバイルデバイス上の写真をクラウドサービス(iCloudやGoogle Photosなど)と同期し、他のデバイスからそれらを表示したいという現実的なシナリオを考えてみましょう。写真の表示という基本的な機能に加えて、このようなサービスは多くの追加機能を提供します。たとえば
- 写真の編集とビデオ効果。
- 「思い出」やアルバムの作成。
- 一連の写真からのビデオモンタージュ。
- …など。
ここでは、例として、そのようなサービスのかなり原始的な実装を使用します。重要なのは、現実世界でWeakRef
とFinalizationRegistry
を一緒に使用できるシナリオを示すことです。
それがどのようなものかを示します。

左側には、写真のクラウドライブラリがあります(サムネイルとして表示されます)。必要な画像を選択し、「コラージュを作成」ボタンをクリックしてコラージュを作成できます。その後、結果のコラージュは画像としてダウンロードできます。
ページの読み込み速度を上げるために、写真は圧縮された品質でダウンロードして表示するのが妥当です。しかし、選択した写真からコラージュを作成するには、フルサイズ品質でダウンロードして使用します。
下図のように、サムネイルの固有サイズは240x240ピクセルです。読み込み速度を上げるために、このサイズが意図的に選択されました。さらに、プレビューモードではフルサイズの画像は必要ありません。

4枚の写真のコラージュを作成する必要があると仮定します。選択してから「コラージュを作成」ボタンをクリックします。この段階で、既知の
weakRefCache
関数は、必要な画像がキャッシュにあるかどうかを確認します。ない場合は、クラウドからダウンロードして、後で使用するようにキャッシュに格納します。これは、選択した画像ごとに発生します。
コンソールの出力に注意すると、どの写真がクラウドからダウンロードされたかがわかります。これはFETCHED_IMAGEで示されています。これはコラージュを作成する最初の試みであるため、この段階では「弱いキャッシュ」はまだ空であり、すべての画像がクラウドからダウンロードされてそこに格納されたことを意味します。
しかし、画像のダウンロードプロセスと並行して、ガベージコレクタによるメモリのクリーンアッププロセスもあります。これは、弱い参照を使用して参照するキャッシュに格納されているオブジェクトが、ガベージコレクタによって削除されることを意味します。そして、ファイナライザが正常に実行され、画像がキャッシュに格納されていたキーが削除されます。CLEANED_IMAGEはそれについて通知します。

次に、結果のコラージュが気に入らず、画像の1つを変更して新しいコラージュを作成することにします。そのためには、不要な画像の選択を解除し、別の画像を選択して、「コラージュを作成」ボタンをもう一度クリックするだけです。

しかし、今回はすべての画像がネットワークからダウンロードされたわけではなく、そのうちの1つは弱いキャッシュから取得されました。CACHED_IMAGEメッセージはそれについて通知します。これは、コラージュ作成時に、ガベージコレクタがまだ画像を削除しておらず、大胆にもキャッシュから取得し、ネットワークリクエストの数を減らし、コラージュ作成プロセスの全体時間を短縮したことを意味します。

画像の1つをもう一度置き換えて新しいコラージュを作成することで、もう少し「遊んで」みましょう。

今回は、結果がさらに印象的です。選択した4枚の画像のうち、3枚は弱いキャッシュから取得され、1枚のみネットワークからダウンロードする必要がありました。ネットワーク負荷の削減は約75%でした。素晴らしいですね。

もちろん、このような動作は保証されておらず、具体的な実装とガベージコレクタの動作に依存することに注意することが重要です。
これに基づいて、すぐに論理的な質問が生じます。ガベージコレクタに依存するのではなく、自分自身でエンティティを管理できる通常のキャッシュを使用しないのはなぜですか?その通りです。ほとんどの場合、WeakRef
とFinalizationRegistry
を使用する必要はありません。
ここでは、興味深い言語機能を使用した非自明なアプローチを使用して、同様の機能の代替実装を示しただけです。それでも、一定で予測可能な結果が必要な場合は、この例に依存することはできません。
この例は、サンドボックスで開くことができます。
要約
WeakRef
- オブジェクトへの弱い参照を作成し、それらへの強い参照がなくなった場合に、ガベージコレクタによってメモリから削除できるように設計されています。これは、過剰なメモリ使用に対処し、アプリケーションでのシステムリソースの使用を最適化するために役立ちます。
FinalizationRegistry
- 強く参照されなくなったオブジェクトが破棄されたときに実行されるコールバックを登録するためのツールです。これにより、オブジェクトに関連付けられたリソースを解放したり、オブジェクトをメモリから削除する前に必要なその他の操作を実行したりできます。
コメント
<code>
タグを使用し、数行の場合は<pre>
タグで囲み、10行を超える場合はサンドボックス(plnkr、jsbin、codepenなど)を使用してください。