WeakRef、すなわち弱参照は多くの(ガベージコレクションを持つ)プログラミング言語に存在する機能です。ちょっと「weakref」でGoogle検索するだけで、Python, Ruby, PHP, Javaにこの概念が存在することが確認できます。
皆さんもよくご存知の通り、JavaScriptもガベージコレクションを持つ言語のひとつです。しかし、残念なことに弱参照はいまだJavaScriptにありませんでした。
もちろん、そんな状況に置かれているJavaScriptに弱参照を導入しようという動きもしっかりとあります。それがWeakRefプロポーザルです。このプロポーザルは現在Stage 2、つまり方向性はおおよそ定まって絶賛仕様策定中という状況です。それゆえに、この記事で解説することの委細は今後変わるかもしれません。しかし大きな方向性はよほどのことがないと変わらないと考えられます。このことを理解し、ぜひWeakRefの登場に備えましょう。
※記事タイトルに「JavaScriptに弱参照がやってくる」とありますが、実際にやってくるのは多分年単位で先です。なお、node.js(v12以上)は--harmony-weak-refs
オプションをつけることで実験的な実装を利用可能です。
WeakRefとは何か
では、いよいよWeakRefとは何かについて解説します。日本語では先程も述べたとおり弱参照ですが、これは参照先のオブジェクトがガベージコレクション対象になってしまうかもしれないような参照です。
参照
ここで「参照」という言葉が出てきましたが、ここでは何らかの方法でオブジェクトにアクセス可能な手段を参照と呼んでいます。この概念を掴むために、次の例を見てみましょう。
let obj = { name: "object 1" };
obj = { name: "object 2" };
変数obj
に{ name: "object 1" }
を入れたあと、次に{ name: "object 2" }
を入れました。最初のオブジェクトをオブジェクト1、次のオブジェクトをオブジェクト2とすると、obj
はまずオブジェクト1が入った後にオブジェクト2が入りました。このとき、オブジェクト2はobj
に入っているのでobj
を通じてアクセス可能ですが、オブジェクト1はもう変数obj
に入っていないので、どうやってもプログラムからアクセス不能です。つまり、オブジェクト2へは参照があり、オブジェクト1へは参照が無いという状態になっています。
ところで、オブジェクト1やオブジェクト2がプログラムに登場した時点で、それらの実体がマシンのメモリ上に作成されます。そもそもプログラム中に登場する全ての値(数値とかも)はマシンが覚えている必要があり、覚えておくために情報を置いておく場所がメモリです。
参照されなくなったオブジェクトはもうプログラムにとっては有っても無くても関係ないどうでもよい存在ですから、メモリから消しても構いません。ところが、ガベージコレクションのある言語では、参照されなくなったオブジェクトを即座にメモリから消去することはあまりありません。もう参照されていない不要なオブジェクトがメモリ上に溜まってからまとめて消す処理をするのが普通です。この処理がガベージコレクションです。
上のプログラムの実行後は、ガベージコレクションによってオブジェクト1が消去されるかもしれません。その一方、オブジェクト2は消去されません。なぜなら、変数obj
を使ったらオブジェクト2を触ることができるからです。参照されているオブジェクトは触られる可能性があるのでメモリ上にデータが無ければいけません。
弱参照
では、話を弱参照に戻しましょう。これはその名の通り弱い参照です。つまり、オブジェクトへの参照はあるけど、弱いので参照先がオブジェクトがガベージコレクションされているかもしれないというものです。よって、いざ参照先のオブジェクトを使おうとしたらもう捨てられていて使えませんという事態になることがあります。
WeakRefの使い方
WeakRef
のAPIはとても単純です。まず、何らかのオブジェクトobj
への弱参照を表すWeakRef
オブジェクトはnew WeakRef(obj)
として作成します。
let obj = { name: "object 1" };
const wref = new WeakRef(obj);
そして、WeakRef
オブジェクトの参照先を得るにはderef
メソッドを用います。上の例の直後にderef()
メソッドを呼ぶと当然返り値はobj
です。
console.log(wref.deref() === obj); // true
もしwref.deref()
を呼ぶまでの間にwref
の参照先がガベージコレクションの対象になって捨てられた場合は、wref.deref()
の返り値はundefined
となります。
ただし、上の例でwref
の参照先がガベージコレクションの対象となるには、まずobj
を通じた当該オブジェクトへの参照を消す必要があります。
let obj = { name: "object 1" }; // objにオブジェクト1を代入
const wref = new WeakRef(obj);
console.log(wref.deref()); // { name: "object 1" };
// オブジェクト1への(弱ではない)参照を無くす
obj = null;
// ここではオブジェクト1への参照はwrefによる弱参照のみ
// しばらくするとwrefの参照先がガベージコレクトされるかも
// するとwref.deref()の返り値はundefinedになる
console.log(wref.deref()); // undefined
WeakRef
の基本はこれだけです。とても簡単ですね。
FinalizationGroup
しかし、実はガベージコレクションに関連してもうひとつセットで提案されているAPIがあります。それがFinalizationGroup
です。このAPIを使うと、オブジェクトがガベージコレクトされた(メモリから消去された)タイミングを検知することができます。
これの使い方もそんなに難しくありません。まずnew FinalizationGroup(callback)
として新しいFinalizationGroup
オブジェクトを作成します。callback
というのは値がガベージコレクトされたときに呼ばれるコールバックですが、これの詳細はあとで説明します。
そして、できたオブジェクトのregister
メソッドを呼ぶことで、ガベージコレクトされたのを検知したいオブジェクトを登録します。
// オブジェクトが捨てられたときに呼ばれるコールバック(詳細は後述)
const handler = iterator => {
for (const key of iterator) {
console.log(key, "がガベージコレクトされました");
}
};
// FinalizationGroupオブジェクトを作成
const group = new FinalizationGroup(handler);
// 適当なオブジェクトを作成
const obj = { name: "object 1" };
// 監視対象に登録
group.register(obj, "オブジェクト1");
このように、register
メソッドの第1引数に監視対象のオブジェクトを渡します。これにより、obj
がガベージコレクトされたらhandler
が呼ばれることになります。
ポイントは第2引数です。これは、第1引数のオブジェクトを表す何らかの値です(別のオブジェクトでも構いませんが)。実は、第1引数のオブジェクトがガベージコレクトされた場合に実際にコールバックに渡されるのは第2引数に指定したほうの値です(holdingsと呼ばれるらしいです)。その理由は、第1引数のオブジェクトはそのタイミングでは既にガベージコレクトされており利用不能になっているからです。そのため、代わりに(既にガベージコレクトされて消えてしまった)オブジェクトを識別するためのものとしてholdingsの値を利用します。
では、いよいよFinalizationGroup
に渡すコールバックの解説をします。何らかのオブジェクト(複数まとめてかもしれません)がガベージコレクトされて消されたとき、コールバック関数にはそれらのオブジェクトに対応するholdingsたちのイテレータが渡されます。イテレータの詳細な説明は省きますが、for-of
でループしたりArray.from(iterator)
で配列に変換できると思っておけば大丈夫です1。
上の例でいえば、obj
がガベージコレクトされた場合はhandler
が呼び出されて、最終的にfor-ofループのkey
に"オブジェクト1"
が入ってくることになります。今回はconsole.log
するだけですが、この得られたholdingsをどう使うかはあなた次第です。
次の例で以上の動作を試すことができます(--harmony-weak-refs
に加えて、ガベージコレクションを起動するgc()
関数を利用可能にする--expose-gc
オプションをnode
に与える必要があります)。
// オブジェクトが捨てられたときに呼ばれるコールバック
const handler = iterator => {
for (const key of iterator) {
console.log(key, "がガベージコレクトされました");
}
};
// FinalizationGroupオブジェクトを作成
const group = new FinalizationGroup(handler);
// 適当なオブジェクトを作成(GCされやすいように1GBのメモリを確保)
let obj = new ArrayBuffer(1024 ** 3);
// 監視対象に登録
group.register(obj, "でかいメモリ");
// objへの(弱くない)参照を消す
obj = null;
// ガベージコレクションを起動
gc();
これを実行すると、(ガベージコレクションの挙動にもよりますが)普通はでかいメモリ がガベージコレクトされました
というログが表示されるはずです。これは、gc()
により1GBのArrayBuffer
がガベージコレクトされて、それに反応してFinalizationGroup
のコールバックが呼ばれたことを意味しています。
以上がFinalizationGroup
の機能です。ちなみに、register
を取り消すunregister
メソッドもあります(使用するためには取り消し用のトークンを新たに用意してregister
の第3引数に渡す必要がありますが詳細は省略します)。
WeakRef
の使いみち
ここまで解説したように、WeakRef
の機能は「弱参照を作る」というたいへんシンプルなものです。また、FinalizationGroup
も「オブジェクトがガベージコレクトされたら教えてくれる」という同じくらいのシンプルさです。上では別々に紹介しましたが、両方ともガベージコレクションに関わる機能ですからもちろん組み合わせられる場面もあるでしょう。
機能がシンプルな分だけ、その使いみちは幅広いでしょう。とはいえ、WeakRef
の典型的な使い道としてはキャッシュを挙げざるを得ません。
WeakRef
は「いつ消えるか分からないオブジェクトへの参照」を持つのが基本的な役割ですから、WeakRef
で弱参照されているオブジェクトは「あったら嬉しいけど無くてもまあ大丈夫なオブジェクト」ということになります。これに当てはまるのがまさにキャッシュでしょう。例えばネットワークからダウンロードしたファイルをキャッシュしておくことで、そのファイルが再び必要になったときに再びダウンロードする時間をかけずにファイルを利用することができます。もしキャッシュしておいたファイルが消えていても、再びダウンロードすればまあ大丈夫です2。
特に、キャッシュは一般に容量を喰いますから、全てのキャッシュをメモリに溜め込んでおくことはできません。不要なキャッシュは捨てることでメモリ使用量を少なくすることができます。
キャッシュされた値をWeakRef
を用いて弱参照で保持しておくことで、このような(メモリが埋まってきたらいらないものを消すという)動作をガベージコレクションに任せることができます。また、FinalizationGroup
と組み合わせることによって、キャッシュされたデータ本体に付随するメタデータの掃除などもできるでしょう。
もう少し具体的な実装例がプロポーザルのページにたくさん載っていますので、気になる方は見てみてください。
WeakMap
・WeakSet
との関係
実は、JavaScriptの既存機能にも弱参照に関連するものはあります。それはWeakMap
やWeakSet
です。WeakMap
は好きなオブジェクトに対して好きな値を紐付けられるデータ構造でしたね。特徴は、WeakMap
からキーとなるオブジェクトへの参照が弱参照であるという点です。つまり、何らかのオブジェクトがWeakMap
のキーとして登録されていてもそのオブジェクトがガベージコレクトされることは妨げられません。
const wmap = new WeakMap();
let obj = { name: "object 1" }; // objにオブジェクト1を代入
// objに対して"foobar"を覚えておく
wmap.set(obj, "foobar");
// ...
// objに対応する値を取り出す
console.log(wmap.get(obj)); // "foobar"
// オブジェクト1への参照を消すとそのうちガベージコレクトされる(wmapからオブジェクト1への参照は弱いので)
obj = null;
ただし、WeakMap
等は自身に登録されているキーを露出するAPIを持ちませんので、「当該オブジェクトを取り出そうとしたけどもう捨てられていた」のような事態は発生しません。これがWeakRef
との大きな違いです。
このように、これまではWeakMap
等の内部でのみ用いられていた弱参照の概念を我々が直に利用できるようにする新しいAPIがWeakRef
であると言えます。
まとめ
この記事ではWeakRefsプロポーザルで提案されているWeakRef
およびFinalizationGroup
というAPIを紹介しました。これらは両方ともガベージコレクションに関するAPIで、オブジェクトがガベージコレクトされるかどうか、あるいはされた場合はどうするかといったことに切り込める面白いAPIです。
使用する場面は限られているかもしれませんが、いざ実用化されたらぜひ一回くらいは使ってみたいですね。