ガベージコレクションの章で学んだように、JavaScriptエンジンは値が「到達可能」で、潜在的に使用できる間はメモリに保持します。
例えば
let john = { name: "John" };
// the object can be accessed, john is the reference to it
// overwrite the reference
john = null;
// the object will be removed from memory
通常、オブジェクトのプロパティや配列または他のデータ構造の要素は、そのデータ構造がメモリにある間、到達可能と見なされ、メモリに保持されます。
例えば、オブジェクトを配列に入れた場合、配列が存続する限り、たとえ他の参照がなくても、オブジェクトも存続します。
このように
let john = { name: "John" };
let array = [ john ];
john = null; // overwrite the reference
// the object previously referenced by john is stored inside the array
// therefore it won't be garbage-collected
// we can get it as array[0]
同様に、通常のMap
でオブジェクトをキーとして使用した場合、Map
が存在する間、そのオブジェクトも存在します。メモリを占有し、ガベージコレクションされない可能性があります。
例えば
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // overwrite the reference
// john is stored inside the map,
// we can get it by using map.keys()
WeakMap
はこの点で根本的に異なります。キーオブジェクトのガベージコレクションを防ぎません。
例で見てみましょう。
WeakMap
Map
とWeakMap
の最初の違いは、キーがプリミティブ値ではなくオブジェクトでなければならないことです。
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "ok"); // works fine (object key)
// can't use a string as the key
weakMap.set("test", "Whoops"); // Error, because "test" is not an object
ここで、オブジェクトをキーとして使用し、そのオブジェクトへの他の参照がない場合、メモリから(そしてマップから)自動的に削除されます。
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // overwrite the reference
// john is removed from memory!
上記の通常のMap
の例と比較してください。john
がWeakMap
のキーとしてのみ存在する場合、マップ(およびメモリ)から自動的に削除されます。
WeakMap
は反復処理とkeys()
、values()
、entries()
メソッドをサポートしていないため、そこからすべてのキーまたは値を取得する方法はありません。
WeakMap
には次のメソッドのみがあります。
なぜこのような制限があるのでしょうか?それは技術的な理由からです。オブジェクトが他のすべての参照を失った場合(上記のコードのjohn
など)、自動的にガベージコレクションされます。しかし、技術的には、いつクリーンアップが行われるかは正確に指定されていません。
JavaScriptエンジンがそれを決定します。メモリクリーンアップをすぐに実行するか、後でより多くの削除が行われたときにクリーンアップを行うかを決定します。したがって、技術的には、WeakMap
の現在の要素数は不明です。エンジンがそれをクリーンアップしたかどうか、または部分的にクリーンアップしたかどうかはわかりません。そのため、すべてのキー/値にアクセスするメソッドはサポートされていません。
では、そのようなデータ構造はどこで必要になるのでしょうか?
ユースケース:追加データ
WeakMap
の主な適用分野は、追加データストレージです。
別のコード(サードパーティライブラリの場合もある)に「属する」オブジェクトを操作していて、それに関連付けられたデータを保存したい場合、そのデータはオブジェクトが存続する間のみ存在する必要があります。そのような場合、WeakMap
がまさに必要なものです。
オブジェクトをキーとして使用してデータをWeakMap
に配置すると、オブジェクトがガベージコレクションされると、そのデータも自動的に消えます。
weakMap.set(john, "secret documents");
// if john dies, secret documents will be destroyed automatically
例を見てみましょう。
例えば、ユーザーの訪問回数を保持するコードがあるとします。情報はマップに保存されます。ユーザーオブジェクトがキーで、訪問回数が値です。ユーザーが離れると(そのオブジェクトがガベージコレクションされると)、その訪問回数を保存したくありません。
Map
を使用したカウント関数の例を次に示します。
// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count
// increase the visits count
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
そして、おそらくそれを使用する別のファイルのコードです。
// 📁 main.js
let john = { name: "John" };
countUser(john); // count his visits
// later john leaves us
john = null;
これで、john
オブジェクトはガベージコレクションされるはずですが、visitsCountMap
のキーであるため、メモリに残ります。
ユーザーを削除する際にはvisitsCountMap
をクリーンアップする必要があります。そうでないと、メモリが無期限に増大します。このようなクリーンアップは、複雑なアーキテクチャでは面倒な作業になる可能性があります。
代わりにWeakMap
を使用することで、それを回避できます。
// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
// increase the visits count
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
これで、visitsCountMap
をクリーンアップする必要がなくなりました。john
オブジェクトがWeakMap
のキーを除いてすべての手段で到達不能になると、そのキーによる情報とともにメモリから削除されます。
ユースケース:キャッシング
もう1つの一般的な例はキャッシングです。関数の結果を保存(キャッシュ)して、同じオブジェクトに対する将来の呼び出しで再利用できるようにすることができます。
そのためには、Map
を使用できます(最適なシナリオではありません)。
// 📁 cache.js
let cache = new Map();
// calculate and remember the result
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculations of the result for */ obj;
cache.set(obj, result);
return result;
}
return cache.get(obj);
}
// Now we use process() in another file:
// 📁 main.js
let obj = {/* let's say we have an object */};
let result1 = process(obj); // calculated
// ...later, from another place of the code...
let result2 = process(obj); // remembered result taken from cache
// ...later, when the object is not needed any more:
obj = null;
alert(cache.size); // 1 (Ouch! The object is still in cache, taking memory!)
同じオブジェクトでprocess(obj)
を複数回呼び出すと、最初に結果を計算し、その後cache
から取得します。欠点は、オブジェクトが不要になったときにcache
をクリーンアップする必要があることです。
Map
をWeakMap
に置き換えると、この問題はなくなります。オブジェクトがガベージコレクションされると、キャッシュされた結果は自動的にメモリから削除されます。
// 📁 cache.js
let cache = new WeakMap();
// calculate and remember the result
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculate the result for */ obj;
cache.set(obj, result);
return result;
}
return cache.get(obj);
}
// 📁 main.js
let obj = {/* some object */};
let result1 = process(obj);
let result2 = process(obj);
// ...later, when the object is not needed any more:
obj = null;
// Can't get cache.size, as it's a WeakMap,
// but it's 0 or soon be 0
// When obj gets garbage collected, cached data will be removed as well
WeakSet
WeakSet
は同様に動作します。
Set
に似ていますが、WeakSet
にはオブジェクトのみを追加できます(プリミティブは追加できません)。- オブジェクトは、他の場所から到達可能である間、セットに存在します。
Set
と同様に、add
、has
、delete
をサポートしますが、size
、keys()
、反復処理はサポートしません。
「弱い」ものであるため、追加ストレージとしても機能します。ただし、任意のデータではなく、「yes/no」の事実を保存するために使用されます。WeakSet
へのメンバーシップは、オブジェクトに関する何かを意味する可能性があります。
例えば、サイトにアクセスしたユーザーを追跡するために、ユーザーをWeakSet
に追加できます。
let visitedSet = new WeakSet();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
visitedSet.add(john); // John visited us
visitedSet.add(pete); // Then Pete
visitedSet.add(john); // John again
// visitedSet has 2 users now
// check if John visited?
alert(visitedSet.has(john)); // true
// check if Mary visited?
alert(visitedSet.has(mary)); // false
john = null;
// visitedSet will be cleaned automatically
WeakMap
とWeakSet
の最も注目すべき制限は、反復処理がないこと、および現在のすべてのコンテンツを取得できないことです。これは不便に思えるかもしれませんが、WeakMap/WeakSet
の主な役割、つまり別の場所で保存/管理されているオブジェクトの「追加」データストレージとしての役割を果たすことを妨げません。
まとめ
WeakMap
は、キーとしてオブジェクトのみを許可し、他の手段ではアクセスできなくなったときに関連付けられた値とともにキーを削除する、Map
のようなコレクションです。
WeakSet
は、オブジェクトのみを保存し、他の手段ではアクセスできなくなったときにオブジェクトを削除する、Set
のようなコレクションです。
それらの主な利点は、オブジェクトへの弱い参照を持つため、ガベージコレクタによって簡単に削除できることです。
これは、clear
、size
、keys
、values
などがサポートされないという犠牲を払って実現されます。
WeakMap
とWeakSet
は、「プライマリ」オブジェクトストレージに加えて「セカンダリ」データ構造として使用されます。オブジェクトがプライマリストレージから削除されると、WeakMap
のキーまたはWeakSet
内にある場合のみ、自動的にクリーンアップされます。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10行を超える場合は、サンドボックス(plnkr、jsbin、codepenなど)を使用してください。