ES6 WeakMapの実際の使い道は?

ECMAScript 6 で導入された WeakMap データ構造の実際の使い方は?

弱いマップのキーは対応する値への強い参照を作るので、弱いマップに挿入された値は、そのキーが生きている限り 決して 消えないことを保証します。

ということのようです。

weakmap.set(key, value);

...は遠回しに言っているに過ぎません。

key.value = value;

具体的にどのようなユースケースがあるのでしょうか?

質問へのコメント (15)
ソリューション

ファンダメンタルズ

WeakMap はガベージコレクションに干渉することなく、外部からオブジェクトを拡張する方法を提供します。 オブジェクトを拡張したいけれど、それがシールされているために拡張できないとき、あるいは外部から拡張されたとき、いつでも WeakMap を適用することが可能です。

WeakMap は key が弱いマップ(辞書)です。つまり、key への参照がすべて失われ、値への参照がなくなれば、value はガベージコレクションされる可能性があります。まず、例題でこれを示し、次に少し説明し、最後に実際の使用例を示して終了しましょう。

例えば、あるAPIを使って、あるオブジェクトを取得するとします。

var obj = getObjectFromLibrary();

さて、そのオブジェクトを利用するメソッドがあります。

function useObj(obj){
   doSomethingWith(obj);
}

このメソッドが特定のオブジェクトで何回呼び出されたかを記録し、それがN回以上発生した場合に報告したいのです。素直に考えれば、Map.Dataを使えばいいと思うだろう。

var map = new Map(); // maps can have object keys
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; // called one more time
    if(called > 10) report(); // Report called more than 10 times
    map.set(obj, called);
}

これは動作しますが、メモリリークが発生します。関数に渡されたライブラリオブジェクトの一つ一つを追跡することになり、ライブラリオブジェクトがガベージコレクションされるのを防いでしまうからです。その代わりに、WeakMap を使用することができます。

var map = new WeakMap(); // create a weak map
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; // called one more time
    if(called > 10) report(); // Report called more than 10 times
    map.set(obj, called);
}

そして、メモリリークは解消されます。

使用例

WeakMap`s によって可能になる、他の方法ではメモリーリークが発生するいくつかのユースケースを紹介します。

  • 特定のオブジェクトに関するプライベートなデータを保持し、そのマップを参照している人だけにアクセスを許可する。よりアドホックなアプローチはprivate-symbolsの提案で実現される予定ですが、それはまだ先の話です。
  • ライブラリオブジェクトに関するデータを、変更したりオーバーヘッドを発生させたりすることなく保持する。
  • JSエンジンが同じ型のオブジェクトに使う隠しクラスで問題を起こさないように、その型のオブジェクトが多数存在する小さなオブジェクトのセットに関するデータを保持する。
  • ブラウザのDOMノードのようなホストオブジェクトに関するデータを保持する。
  • 外部からオブジェクトに機能を追加する(他の回答のイベントエミッターの例のように)。

Let's look at a real use

外部からオブジェクトを拡張するために使用することができます。Node.jsの現実の世界から、実用的な(脚色された、ある種の現実的な-主張をするための)例を挙げましょう。

例えば、あなたがNode.jsで Promise オブジェクトを持っているとします。ここで、あなたは現在拒否されているすべてのプロミスを追跡したいのですが、それらへの参照が存在しない場合にガベージコレクションされないようにしたいのです。

今、あなたは明白な理由のためにネイティブオブジェクトにプロパティを追加したいとは思っていません。もし、プロミスへの参照を保持するならば、ガベージコレクションが起こらないので、メモリリークを引き起こしていることになります。もしあなたが参照を保持しないなら、あなたは個々のプロミスについての追加情報を保存することができません。プロミスのIDを保存することを含むすべてのスキームは、本質的にあなたがそれへの参照を必要とすることを意味します。

WeakMapsの入力

WeakMapはkeysが弱いことを意味します。弱いマップを列挙したり、そのすべての値を取得する方法はありません。弱いマップでは、キーに基づいてデータを保存し、キーがガベージコレクションされたときに、値もガベージコレクションされます。

つまり、プロミスがあればそれに関する状態を保存でき、そのオブジェクトはまだガベージコレクションされる可能性があります。後でオブジェクトへの参照を得た場合、それに関連する状態があるかどうかを確認し、それを報告することができます。

これは Petka Antonov による unhandled rejection hooksthis として実装するために使われました。

process.on('unhandledRejection', function(reason, p) {
    console.log("Unhandled Rejection at: Promise ", p, " reason: ", reason);
    // application specific logging, throwing an error, or other logic here
});

約束に関する情報をマップに保存し、拒否された約束がいつ処理されたかを知ることができます。

解説 (8)

この回答は偏った内容で、実戦では使えないと思われます。このまま読んで、実験以外の実際の選択肢としては考えないでください。

ユースケースとしては、リスナー用の辞書として使用することが考えられます、私の同僚でそうしている人がいます。この方法だとどんなリスナーも直接対象になるので、とても便利です。さようなら listener.on.

しかし、より抽象的な視点から見ると、WeakMapは基本的にあらゆるものへのアクセスを非物質化するために特に強力なもので、この構造の性質によってすでに暗示されているので、メンバーを分離するための名前空間は必要ありません。冗長なオブジェクトのキーを置き換えることで、メモリを大幅に改善することができると思います(分解すれば可能ですが)。


次を読む前に

Benjamin Gruenbaum]1 が指摘したように (彼の答えが私の答えの上になければ、それを見てください :p) 、この問題は通常の Map では解決できなかったでしょう、それはリークしてしまうからです。


以下は私の同僚の実際のコードです (共有してくれた に感謝します)。

フルソースはこちら、上で話したリスナー管理についてです(仕様も見てみてください)。

var listenableMap = new WeakMap();

export function getListenable (object) {
    if (!listenableMap.has(object)) {
        listenableMap.set(object, {});
    }

    return listenableMap.get(object);
}

export function getListeners (object, identifier) {
    var listenable = getListenable(object);
    listenable[identifier] = listenable[identifier] || [];

    return listenable[identifier];
}

export function on (object, identifier, listener) {
    var listeners = getListeners(object, identifier);

    listeners.push(listener);
}

export function removeListener (object, identifier, listener) {
    var listeners = getListeners(object, identifier);

    var index = listeners.indexOf(listener);
    if(index !== -1) {
        listeners.splice(index, 1);
    }
}

export function emit (object, identifier, ...args) {
    var listeners = getListeners(object, identifier);

    for (var listener of listeners) {
        listener.apply(object, args);
    }
}
解説 (6)

WeakMap` はカプセル化と情報隠蔽のためにうまく機能する

WeakMapは ES6 以降で利用可能である。WeakMap はキーと値のペアのコレクションであり、キーはオブジェクトでなければならない。次の例では、2 つの項目からなる WeakMap を作成する。

var map = new WeakMap();
var pavloHero = {first: "Pavlo", last: "Hero"};
var gabrielFranco = {first: "Gabriel", last: "Franco"};
map.set(pavloHero, "This is Hero");
map.set(gabrielFranco, "This is Franco");
console.log(map.get(pavloHero));//This is Hero

set()メソッドを用いて、オブジェクトと他の項目(この例では文字列)の関連付けを定義しています。オブジェクトに関連付けられたアイテムを取得するためにget()メソッドを使いました。WeakMaps の興味深い点は、マップ内のキーへの弱い参照を保持していることです。弱い参照ということは、オブジェクトが破棄されると、ガベージコレクタが WeakMap からエントリ全体を削除するので、メモリが解放されることになります。

var TheatreSeats = (function() {
  var priv = new WeakMap();
  var _ = function(instance) {
    return priv.get(instance);
  };

  return (function() {
      function TheatreSeatsConstructor() {
        var privateMembers = {
          seats: []
        };
        priv.set(this, privateMembers);
        this.maxSize = 10;
      }
      TheatreSeatsConstructor.prototype.placePerson = function(person) {
        _(this).seats.push(person);
      };
      TheatreSeatsConstructor.prototype.countOccupiedSeats = function() {
        return _(this).seats.length;
      };
      TheatreSeatsConstructor.prototype.isSoldOut = function() {
        return _(this).seats.length >= this.maxSize;
      };
      TheatreSeatsConstructor.prototype.countFreeSeats = function() {
        return this.maxSize - _(this).seats.length;
      };
      return TheatreSeatsConstructor;
    }());
})()
解説 (1)

私は、不変のオブジェクトをパラメーターとして取り込む関数の心配のないメモ作成のキャッシュに「WeakMap」を使用します。

記憶は、「値を計算した後、キャッシュして、再度計算する必要がないようにする」という派手な言い方です。

次に例を示します。

// using immutable.js from here https://facebook.github.io/immutable-js/

const memo = new WeakMap();

let myObj = Immutable.Map({a: 5, b: 6});

function someLongComputeFunction (someImmutableObj) {
  // if we saved the value, then return it
  if (memo.has(someImmutableObj)) {
    console.log('used memo!');
    return memo.get(someImmutableObj);
  }

  // else compute, set, and return
  const computedValue = someImmutableObj.get('a') + someImmutableObj.get('b');
  memo.set(someImmutableObj, computedValue);
  console.log('computed value');
  return computedValue;
}

someLongComputeFunction(myObj);
someLongComputeFunction(myObj);
someLongComputeFunction(myObj);

// reassign
myObj = Immutable.Map({a: 7, b: 8});

someLongComputeFunction(myObj);
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script>

注意すべきいくつかのこと:

  • Immutable.jsオブジェクトは、新しいオブジェクトを変更すると(新しいポインターを使用して)返されるため、WeakMapでキーとして使用すると、同じ計算値が保証されます。
  • オブジェクト(キーとして使用)がガベージを取得すると、WeakMapの計算値も収集されるため、WeakMapはメモに最適です。
解説 (1)

𝗠𝗲𝘁𝗮𝗱𝗮𝘁𝗮。

弱いマップを使用して、ガベージコレクションを妨害したり、同僚をコードで怒らせたりすることなく、DOM要素に関するメタデータを保存できます。 たとえば、それらを使用して、Webページのすべての要素を数値で表示できます。

𝗪𝗶𝘁𝗵𝗼𝘂𝘁 𝗪#120306;𝗮 &#120

var elements = document.getElementsByTagName('*'),
  i = -1, len = elements.length;

while (++i !== len) {
  // Production code written this poorly makes me want to cry:
  elements[i].lookupindex = i;
  elements[i].elementref = [];
  elements[i].elementref.push( elements[Math.pow(i, 2) % len] );
}

// Then, you can access the lookupindex's
// For those of you new to javascirpt, I hope the comments below help explain 
// how the ternary operator (?:) works like an inline if-statement
document.write(document.body.lookupindex + '<br />' + (
    (document.body.elementref.indexOf(document.currentScript) !== -1)
    ? // if(document.body.elementref.indexOf(document.currentScript) !== -1){
    "true"
    : // } else {
    "false"
  )   // }
);

𝗨𝘀𝗶𝗻𝗴 𝗪𝗲𝗮𝗸𝗠 &#120

var DOMref = new WeakMap(),
  __DOMref_value = Array,
  __DOMref_lookupindex = 0,
  __DOMref_otherelement = 1,
  elements = document.getElementsByTagName('*'),
  i = -1, len = elements.length, cur;

while (++i !== len) {
  // Production code written this greatly makes me want to 😊:
  cur = DOMref.get(elements[i]);
  if (cur === undefined)
    DOMref.set(elements[i], cur = new __DOMref_value)

  cur[__DOMref_lookupindex] = i;
  cur[__DOMref_otherelement] = new WeakSet();
  cur[__DOMref_otherelement].add( elements[Math.pow(i, 2) % len] );
}

// Then, you can access the lookupindex's
cur = DOMref.get(document.body)
document.write(cur[__DOMref_lookupindex] + '<br />' + (
    cur[__DOMref_otherelement].has(document.currentScript)
    ? // if(cur[__DOMref_otherelement].has(document.currentScript)){
    "true"
    : // } else {
    "false"
  )   // }
);

𝗧𝗵𝗲 𝗗𝗶𝗳𝗳𝗲𝗿𝗯 &#120

弱者マップのバージョンが長いという事実は別として、違いは無視できるように見えるかもしれませんが、上記の2つのコード間に大きな違いがあります。 コードの最初のスニペットでは、弱いマップなしで、コードの一部はDOM要素間のあらゆる方法を参照します。 これにより、DOM要素がゴミ収集されるのを防ぎます。 Math.po w(i、2)%len]は、誰も使用しない奇妙なボールのように見えるかもしれませんが、もう一度考えてください。多くのプロダクションコードには、ドキュメント全体で跳ね返るDOM参照があります。 次に、2番目のコードでは、要素へのすべての参照が弱いため、ノードを削除すると、ブラウザはノードが使用されていない(コードで到達できない)と判断できます。したがって、メモリから削除します。 メモリの使用について心配する必要がある理由。, そしてメモリアンカー。 (未使用の要素がメモリに保持されるコードの最初のスニペットなど。) メモリの使用が増えると、ブラウザのGC試行が増えるからです。 (ブラウザのクラッシュを回避するためにメモリを解放しようとします。) ブラウジングエクスペリエンスが遅くなり、ブラウザがクラッシュする場合があります。

これらのポリフィルについては、自分のライブラリをお勧めします(ここ@ githubにあります)。 これは非常に軽量なライブラリであり、他のポリフィルにあるような過度に複雑なフレームワークなしで単純にポリフィルします。

〜幸せなコーディング。!

解説 (6)