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

**WeakRef**、すなわち**弱参照**は多くの（ガベージコレクションを持つ）プログラミング言語に存在する機能です。ちょっと「weakref」でGoogle検索するだけで、Python, Ruby, PHP, Javaにこの概念が存在することが確認できます。

皆さんもよくご存知の通り、JavaScriptもガベージコレクションを持つ言語のひとつです。しかし、残念なことに弱参照はいまだJavaScriptにありませんでした。

もちろん、そんな状況に置かれているJavaScriptに**弱参照**を導入しようという動きもしっかりとあります。それが[WeakRefプロポーザル](https://github.com/tc39/proposal-weakrefs)です。このプロポーザルは現在Stage 2、つまり方向性はおおよそ定まって絶賛仕様策定中という状況です。それゆえに、この記事で解説することの委細は今後変わるかもしれません。しかし大きな方向性はよほどのことがないと変わらないと考えられます。このことを理解し、ぜひWeakRefの登場に備えましょう。

※記事タイトルに「JavaScriptに弱参照がやってくる」とありますが、実際にやってくるのは多分年単位で先です。なお、node.js（v12以上）は`--harmony-weak-refs`オプションをつけることで実験的な実装を利用可能です。

## WeakRefとは何か

では、いよいよ**WeakRef**とは何かについて解説します。日本語では先程も述べたとおり**弱参照**ですが、これは**参照先のオブジェクトがガベージコレクション対象になってしまうかもしれないような参照**です。

### 参照

ここで「参照」という言葉が出てきましたが、ここでは何らかの方法でオブジェクトにアクセス可能な手段を参照と呼んでいます。この概念を掴むために、次の例を見てみましょう。

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

```js:WeakRefの使用例
let obj = { name: "object 1" };

const wref = new WeakRef(obj);
```

そして、`WeakRef`オブジェクトの参照先を得るには`deref`メソッドを用います。上の例の直後に`deref()`メソッドを呼ぶと当然返り値は`obj`です。

```js
console.log(wref.deref() === obj); // true
```

もし`wref.deref()`を呼ぶまでの間に`wref`の参照先がガベージコレクションの対象になって捨てられた場合は、`wref.deref()`の返り値は`undefined`となります。

ただし、上の例で`wref`の参照先がガベージコレクションの対象となるには、まず`obj`を通じた当該オブジェクトへの参照を消す必要があります。

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

```js: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)`で配列に変換できると思っておけば大丈夫です[^iterator_reason]。

[^iterator_reason]: 配列などではなくイテレータで渡される理由は、全部ではなく途中まで処理する（残りは後回しにする）ことができるようにするためです。

上の例でいえば、`obj`がガベージコレクトされた場合は`handler`が呼び出されて、最終的にfor-ofループの`key`に`"オブジェクト1"`が入ってくることになります。今回は`console.log`するだけですが、この得られたholdingsをどう使うかはあなた次第です。

次の例で以上の動作を試すことができます（`--harmony-weak-refs`に加えて、ガベージコレクションを起動する`gc()`関数を利用可能にする`--expose-gc`オプションを`node`に与える必要があります）。

```js
// オブジェクトが捨てられたときに呼ばれるコールバック
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`で弱参照されているオブジェクトは「あったら嬉しいけど無くてもまあ大丈夫なオブジェクト」ということになります。これに当てはまるのがまさに**キャッシュ**でしょう。例えばネットワークからダウンロードしたファイルをキャッシュしておくことで、そのファイルが再び必要になったときに再びダウンロードする時間をかけずにファイルを利用することができます。もしキャッシュしておいたファイルが消えていても、再びダウンロードすればまあ大丈夫です[^memory_cache]。

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

特に、キャッシュは一般に容量を喰いますから、全てのキャッシュをメモリに溜め込んでおくことはできません。不要なキャッシュは捨てることでメモリ使用量を少なくすることができます。

キャッシュされた値を`WeakRef`を用いて弱参照で保持しておくことで、このような（メモリが埋まってきたらいらないものを消すという）動作をガベージコレクションに任せることができます。また、`FinalizationGroup`と組み合わせることによって、キャッシュされたデータ本体に付随するメタデータの掃除などもできるでしょう。

もう少し具体的な実装例が[プロポーザルのページ](https://github.com/tc39/proposal-weakrefs)にたくさん載っていますので、気になる方は見てみてください。


## `WeakMap`・`WeakSet`との関係

実は、JavaScriptの既存機能にも弱参照に関連するものはあります。それは`WeakMap`や`WeakSet`です。`WeakMap`は好きなオブジェクトに対して好きな値を紐付けられるデータ構造でしたね。特徴は、`WeakMap`からキーとなるオブジェクトへの参照が**弱参照**であるという点です。つまり、何らかのオブジェクトが`WeakMap`のキーとして登録されていてもそのオブジェクトがガベージコレクトされることは妨げられません。

```js: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プロポーザル](https://github.com/tc39/proposal-weakrefs)で提案されている`WeakRef`および`FinalizationGroup`というAPIを紹介しました。これらは両方ともガベージコレクションに関するAPIで、オブジェクトがガベージコレクトされるかどうか、あるいはされた場合はどうするかといったことに切り込める面白いAPIです。

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