ECMAScript 6 で導入された WeakMap
データ構造の実際の使い方は?
弱いマップのキーは対応する値への強い参照を作るので、弱いマップに挿入された値は、そのキーが生きている限り 決して 消えないことを保証します。
ということのようです。
weakmap.set(key, value);
...は遠回しに言っているに過ぎません。
key.value = value;
具体的にどのようなユースケースがあるのでしょうか?
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 によって可能になる、他の方法ではメモリーリークが発生するいくつかのユースケースを紹介します。
外部からオブジェクトを拡張するために使用することができます。Node.jsの現実の世界から、実用的な(脚色された、ある種の現実的な-主張をするための)例を挙げましょう。
例えば、あなたがNode.jsで Promise
オブジェクトを持っているとします。ここで、あなたは現在拒否されているすべてのプロミスを追跡したいのですが、それらへの参照が存在しない場合にガベージコレクションされないようにしたいのです。
今、あなたは明白な理由のためにネイティブオブジェクトにプロパティを追加したいとは思っていません。もし、プロミスへの参照を保持するならば、ガベージコレクションが起こらないので、メモリリークを引き起こしていることになります。もしあなたが参照を保持しないなら、あなたは個々のプロミスについての追加情報を保存することができません。プロミスのIDを保存することを含むすべてのスキームは、本質的にあなたがそれへの参照を必要とすることを意味します。
WeakMapはkeysが弱いことを意味します。弱いマップを列挙したり、そのすべての値を取得する方法はありません。弱いマップでは、キーに基づいてデータを保存し、キーがガベージコレクションされたときに、値もガベージコレクションされます。
つまり、プロミスがあればそれに関する状態を保存でき、そのオブジェクトはまだガベージコレクションされる可能性があります。後でオブジェクトへの参照を得た場合、それに関連する状態があるかどうかを確認し、それを報告することができます。
これは Petka Antonov による unhandled rejection hooks を this として実装するために使われました。
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
});
約束に関する情報をマップに保存し、拒否された約束がいつ処理されたかを知ることができます。
ユースケースとしては、リスナー用の辞書として使用することが考えられます、私の同僚でそうしている人がいます。この方法だとどんなリスナーも直接対象になるので、とても便利です。さようなら 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);
}
}
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()メソッドを使いました。WeakMap
s の興味深い点は、マップ内のキーへの弱い参照を保持していることです。弱い参照ということは、オブジェクトが破棄されると、ガベージコレクタが 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;
}());
})()
私は、不変のオブジェクトをパラメーターとして取り込む関数の心配のないメモ作成のキャッシュに「WeakMap」を使用します。
記憶は、「値を計算した後、キャッシュして、再度計算する必要がないようにする」という派手な言い方です。
次に例を示します。
<。!-スニペットを開始:js hide:true console:true babel:false -->。
// 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>
<。!-終了スニペット-->。
注意すべきいくつかのこと:
弱いマップを使用して、ガベージコレクションを妨害したり、同僚をコードで怒らせたりすることなく、DOM要素に関するメタデータを保存できます。 たとえば、それらを使用して、Webページのすべての要素を数値で表示できます。
<。!-スニペットを開始:js hide:false console:false babel:false -->。
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"
) // }
);
<。!-終了スニペット-->。
<。!-スニペットを開始:js hide:false console:false babel:false -->。
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"
) // }
);
<。!-終了スニペット-->。
弱者マップのバージョンが長いという事実は別として、違いは無視できるように見えるかもしれませんが、上記の2つのコード間に大きな違いがあります。 コードの最初のスニペットでは、弱いマップなしで、コードの一部はDOM要素間のあらゆる方法を参照します。 これにより、DOM要素がゴミ収集されるのを防ぎます。 Math.po w(i、2)%len]
は、誰も使用しない奇妙なボールのように見えるかもしれませんが、もう一度考えてください。多くのプロダクションコードには、ドキュメント全体で跳ね返るDOM参照があります。 次に、2番目のコードでは、要素へのすべての参照が弱いため、ノードを削除すると、ブラウザはノードが使用されていない(コードで到達できない)と判断できます。したがって、メモリから削除します。 メモリの使用について心配する必要がある理由。, そしてメモリアンカー。 (未使用の要素がメモリに保持されるコードの最初のスニペットなど。) メモリの使用が増えると、ブラウザのGC試行が増えるからです。 (ブラウザのクラッシュを回避するためにメモリを解放しようとします。) ブラウジングエクスペリエンスが遅くなり、ブラウザがクラッシュする場合があります。
これらのポリフィルについては、自分のライブラリをお勧めします(ここ@ githubにあります)。 これは非常に軽量なライブラリであり、他のポリフィルにあるような過度に複雑なフレームワークなしで単純にポリフィルします。
<。!-人々がこの愚かなことについて私を引き裂き始めた場合に備えて: これらの例は、文字通り単語ごとに解釈すると、おそらく実用的なアプリケーションがないことに気づきます。多くのWebページプロジェクトが何らかの方法でDOMを動的に変更し、静的に生成された参照がすぐに古くなるためです。 ただし、これはではなくのプロダクションコードであり、WeakMapsのポイントを取得するのに役立つことを目的とした例にすぎません。-->。
〜幸せなコーディング。!