6時だョ!!全員集合!!

Rails・JavaScrictを中心にアウトプットします。

2022年3月8日 [ES2015] Map/Set - WeakMap (JavaScript Primer) WeakMap

WeakMap

WeakMapは、Mapと同じくマップを扱うためのビルトインオブジェクトです。 Mapと違う点は、キーを弱い参照(Weak Reference)で持つことです。

弱い参照とは

ガベージコレクションによるオブジェクトの解放を妨げない特殊な性質です。 メモリ管理の仕組みで学んだ、変数の値の結果を参照するものがあればガベージコレクションから解放されないという特性が、weakMapではあえて働かないという事です。

  • キーに紐づいた値も削除されます。
  • メモリリークを起こしません。

メモリリークとは

メモリリークとは、コンピュータで実行中のプログラムが確保したメモリ領域の解放を忘れたまま放置してしまうこと。 動作の不具合を招くバグ(欠陥)の一種

  • 花見での場所取りをイメージするとわかりやすいです。
  • 誰か1人が敷きシートを複数枚持っており、場所取りをしました。
  • その人は、その敷きシートを敷いたまま別の場所で敷きシートをして花見をしています。
  • 最初の敷きシート場所は使っていないのに他の方が使えませんし、みんなの使える場所が減ってしまいました。

次のコードでは、最初にobjには{}を設定し、WeakMapではそのobjをキーにして値("value")を設定しています。 次にobjに別の値(ここではnull)を代入すると、objが元々参照していた{}という値はどこからも参照されなくなります。 このときWeakMapは{}への弱い参照を持っていますが、弱い参照はGCを妨げないため、{}は不要になった値としてGCによりメモリから解放されます。

同時に、WeakMapは解放されたオブジェクト({})をキーにしてひもづいていた値("value")を破棄できます。 ただし、どのタイミングで実際にメモリから解放するかは、JavaScriptエンジンの実装に依存します。

const map = new WeakMap();
// キーとなるオブジェクト
let obj = {};
// {} への参照をキーに値をセットする
map.set(obj, "value");
// {} への参照を破棄する
obj = null;
// GCが発生するタイミングでWeakMapから値が破棄される
  • {}はnullが再代入された時点でWeakMapから弱い参照を持っているが、メモリから解放されます。
  • {}に紐づいていた値("value")も破棄されます。
  • 実際のメモリから解放されるタイミングはJavaScriptエンジンの実装に依存します。

WeakMapはイテレータの性質を持つオブジェクトではありません。

  • keyを列挙するkeysメソッドやsizeプロパティは存在しません。
  • エントリーを複数持てません。
  • keysメソッドを出力するとイテレーターオブジェクトなので、イテラブルなオブジェクトでないWeakMapに存在するはずがありません。

要素を複数追加しようとするとTypeErrorが発生しました。

const wMap = new WeakMap();

wMap.set("key1","value1");
wMap.set("key2","value2");

TypeError: Invalid value used as weak map key

WeakMapを使う主なケース

クラスにプライベートの値を格納します。

this (クラスインスタンス) を WeakMap のキーにすることで、インスタンスの外からはアクセスできない値を保持できます。 また、クラスインスタンスが参照されなくなったときには自動的に解放されます。

WeakMapの主な使い方のひとつは、クラスにプライベートの値を格納することです。 this (クラスインスタンス) を WeakMap のキーにすることで、インスタンスの外からはアクセスできない値を保持できます。 また、クラスインスタンスが参照されなくなったときには自動的に解放されます。

次のコードでは、オブジェクトが発火するイベントのリスナー関数(イベントリスナー)を WeakMap で管理しています。 イベントリスナーとは、イベントが発生したときに呼び出される関数のことです。 このマップをMapで実装してしまうと、明示的に削除されるまでイベントリスナーはメモリ上に残り続けます。 ここでWeakMapを使うと、addListener メソッドに渡されたlistenerは EventEmitter インスタンスが参照されなくなった際、自動的に解放されます。

// イベントリスナーを管理するマップ
const listenersMap = new WeakMap();

class EventEmitter {
    addListener(listener) {
        // this にひもづいたリスナーの配列を取得する
        const listeners = listenersMap.get(this) ?? [];
        const newListeners = listeners.concat(listener);
        // this をキーに新しい配列をセットする
        listenersMap.set(this, newListeners);
    }
}

// 上記クラスの実行例

let eventEmitter = new EventEmitter();
// イベントリスナーを追加する
eventEmitter.addListener(() => {
    console.log("イベントが発火しました");
});
// eventEmitterへの参照がなくなったことで自動的にイベントリスナーが解放される
eventEmitter = null;
  • addListenerの引数としてリスナー関数を渡します。 リスナー関数(イベントリスナー)とは、イベントが発生したときに呼び出される関数のことです。
  • weakMapではなくMapで定義すると強い参照になるため、nullが再代入されて出力されなくなったイベントリスナーがメモリ上に残ってしまいます。
  • エントリーのキーにthisをセットしているため、インスタンスの外からエントリーの値にアクセスすることはできないためプライベートな値を格納できています。
  • concatは配列の末尾に配列の要素を結合するメソッドです。
  • listenersMap.set()でWeakMapのエントリーのキーにthis、valueにイベントリスナーを追加した配列をエントリーに追加し、イベントが実行されるたびにWeakMapのvalue要素が更新される実装になっています。
  • setでthisを使ってインスタンス変数をキーにして参照している点がポイントです。
  • eventEmiterに別の値を再代入することでweakMapでは弱い参照になるため自動的にイベントリスナーが解放されます。

WeakMapを使う主なケース2

また、あるオブジェクトから計算した結果を一時的に保存する用途でもよく使われます。 次の例ではHTML要素の高さを計算した結果を保存して、2回目以降に同じ計算をしないようにしています。

const cache = new WeakMap();

function getHeight(element) {
    if (cache.has(element)) {
        return cache.get(element);
    }
    const height = element.getBoundingClientRect().height;
    // elementオブジェクトに対して高さをひもづけて保存している
    cache.set(element, height);
    return height;
}
  • このコードは何度実行しても結果値は同じです。
  • 2回目以降はif文がtrueとなるためif文の中身が実行され1度目の結果値が返ります。

Element.getBoundingClientRect()

イメージしにかったためQiita記事のコードを拝借しました。

要素(Element)に対してgetBoundingClientRect()を使用するとその要素の高さや座標などの情報が返されます。

let image = (<HTMLAnchorElement>document.getElementById("image"));
let image_domRect = image.getBoundingClientRect();
console.log(image_domRect)

以下のような返り値が得られます。

DOMRect {x: 677.484375, y: 269, width: 143.015625, height: 15, top: 269, …}
bottom: 284
height: 15
left: 677.484375
right: 820.5
top: 269
width: 143.015625
x: 677.484375
y: 269
__proto__: DOMRect

[コラム] キーの等価性とNaN

Mapに値をセットする際のキーにはあらゆるオブジェクトが使えます。 このときのマップが特定のキーをすでに持っているか、つまり挿入と上書きの判定は基本的に===演算子と同じです。

ただし、キーがNaNの扱いだけが例外的に違います。Mapにおけるキーの比較では、NaN同士は常に等価であるとみなされます。 この挙動はSame-value-zeroアルゴリズムと呼ばれます。

次のコードでは、NaN同士の===の比較結果がfalseになるのに対して、MapのキーではNaN同士の比較結果が一致していることがわかります。

const map = new Map();
map.set(NaN, "value");
// NaNは===で比較した場合は常にfalse
console.log(NaN === NaN); // => false
// MapはNaN同士を比較できる
console.log(map.has(NaN)); // => true
console.log(map.get(NaN)); // => "value"
  • Mapの場合keyにアクセスする際は暗黙的に厳密等価演算子で判定されています。
  • Mapを使わずにNaNを厳密等価演算子で比較するとfalseになります。
  • NaNはMapのキーにして比較するとtrueを返すので各メソッドが使用可能です。

復習

再認識するため本ブログの過去記事からNaNのまとめを持ってきました。

  • 以上の知識を踏まえて今回のコラムを読むとしっかり理解できました。

参考

WeakMap

2021年10月12日 JavaScript (JavaScript Primer) データ型とリテラル2