Help us understand the problem. What is going on with this article?

WeakRef: JavaScriptに弱参照がやってくる(ついでにFinalizationも)

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)として作成します。

WeakRefの使用例
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メソッドを呼ぶことで、ガベージコレクトされたのを検知したいオブジェクトを登録します。

FinalizationGroupの使用例
// オブジェクトが捨てられたときに呼ばれるコールバック(詳細は後述)
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と組み合わせることによって、キャッシュされたデータ本体に付随するメタデータの掃除などもできるでしょう。

もう少し具体的な実装例がプロポーザルのページにたくさん載っていますので、気になる方は見てみてください。

WeakMapWeakSetとの関係

実は、JavaScriptの既存機能にも弱参照に関連するものはあります。それはWeakMapWeakSetです。WeakMapは好きなオブジェクトに対して好きな値を紐付けられるデータ構造でしたね。特徴は、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です。

使用する場面は限られているかもしれませんが、いざ実用化されたらぜひ一回くらいは使ってみたいですね。


  1. 配列などではなくイテレータで渡される理由は、全部ではなく途中まで処理する(残りは後回しにする)ことができるようにするためです。 

  2. ここではメモリ上にキャッシュする場合の話をしています。ファイルシステム上にキャッシュすることもあるかもしれませんが、それはまた別の話です。 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away