「ガベージ コレクション」の章からわかるように、JavaScript エンジンは値が「到達可能」であり、使用できる可能性がある間はメモリ内に値を保持します。
例えば:
let john = { 名前: "ジョン" }; // オブジェクトにアクセスできます。john はそのオブジェクトへの参照です // 参照を上書きします ジョン = null; // オブジェクトはメモリから削除されます
通常、オブジェクトのプロパティ、配列または別のデータ構造の要素は、そのデータ構造がメモリ内にある間、到達可能であるとみなされ、メモリ内に保持されます。
たとえば、オブジェクトを配列に入れると、その配列が生きている間は、他の参照が存在しなくても、オブジェクトも同様に生き続けます。
このような:
let john = { 名前: "ジョン" }; 配列 = [ ジョン ]; とします。 ジョン = null; // 参照を上書きします // john が以前に参照したオブジェクトは配列内に格納されます // したがって、ガベージコレクションされません // array[0] として取得できます
それと同様に、オブジェクトを通常のMap
のキーとして使用する場合、 Map
存在する間、そのオブジェクトも同様に存在します。メモリを占有するため、ガベージ コレクションされない可能性があります。
例えば:
let john = { 名前: "ジョン" }; let Map = new Map(); map.set(ジョン, "..."); ジョン = null; // 参照を上書きします // ジョンはマップ内に保存されます。 // map.keys() を使用して取得できます
WeakMap
この点で根本的に異なります。キーオブジェクトのガベージコレクションは妨げられません。
例でそれが何を意味するかを見てみましょう。
Map
とWeakMap
の最初の違いは、キーはプリミティブ値ではなくオブジェクトである必要があることです。
letweakMap = new WeakMap(); obj = {} にします。 weakMap.set(obj, "ok"); // 正常に動作します (オブジェクトキー) // 文字列をキーとして使用することはできません weakMap.set("テスト", "おっと"); // 「test」はオブジェクトではないのでエラー
ここで、オブジェクトをキーとして使用し、そのオブジェクトへの参照が他にない場合、そのオブジェクトはメモリ (およびマップ) から自動的に削除されます。
let john = { 名前: "ジョン" }; letweakMap = new WeakMap(); weakMap.set(ジョン, "..."); ジョン = null; // 参照を上書きします // ジョンがメモリから削除されました!
上記の通常のMap
例と比較してください。ここで、 john
WeakMap
のキーとしてのみ存在する場合、マップ (およびメモリ) から自動的に削除されます。
WeakMap
反復とメソッドkeys()
、 values()
、 entries()
をサポートしていないため、そこからすべてのキーまたは値を取得する方法はありません。
WeakMap
は次のメソッドのみがあります。
weakMap.set(key, value)
weakMap.get(key)
weakMap.delete(key)
weakMap.has(key)
なぜそのような制限があるのでしょうか?それは技術的な理由によるものです。オブジェクトが他のすべての参照を失った場合 (上記のコードのjohn
など)、そのオブジェクトは自動的にガベージ コレクションされます。ただし、技術的には、クリーンアップがいつ行われるかは正確には指定されていません。
JavaScript エンジンがそれを決定します。メモリのクリーンアップをすぐに実行するか、待機して後でさらに削除が発生したときにクリーンアップを実行するかを選択できます。したがって、技術的には、 WeakMap
の現在の要素数は不明です。エンジンがクリーンアップしたかどうか、あるいは部分的にクリーンアップした可能性があります。そのため、すべてのキー/値にアクセスするメソッドはサポートされていません。
さて、そのようなデータ構造はどこに必要なのでしょうか?
WeakMap
の主な応用分野は、追加のデータ ストレージです。
別のコード、場合によってはサードパーティのライブラリに「属する」オブジェクトを操作していて、そのオブジェクトが生きている間だけ存在すべき、それに関連付けられたデータを保存したい場合、 WeakMap
がまさにそれです。必要です。
オブジェクトをキーとして使用して、データをWeakMap
に配置します。オブジェクトがガベージ コレクションされると、そのデータも自動的に消えます。
weakMap.set(ジョン、「秘密文書」); // ジョンが死亡した場合、機密文書は自動的に破棄されます
例を見てみましょう。
たとえば、ユーザーの訪問数を保持するコードがあります。情報はマップに保存されます。ユーザー オブジェクトがキー、訪問回数が値です。ユーザーが離れると (オブジェクトがガベージ コレクションされると)、ユーザーの訪問数は保存されなくなります。
Map
を使用したカウント関数の例を次に示します。
//?訪問数.js visitCountMap = new Map(); にしましょう。 // マップ: ユーザー => 訪問数 // 訪問数を増やします 関数 countUser(user) { let count = visitCountMap.get(user) || 0; visitCountMap.set(ユーザー, カウント + 1); }
そして、これがコードの別の部分、おそらくそれを使用する別のファイルです。
//?メイン.js let john = { 名前: "ジョン" }; countUser(ジョン); // 彼の訪問をカウントします // その後、ジョンは私たちのもとを去りました ジョン = null;
ここで、 john
オブジェクトはガベージ コレクションされるはずですが、 visitsCountMap
のキーであるため、メモリ内に残ります。
ユーザーを削除するときは、 visitsCountMap
クリーンアップする必要があります。そうしないと、メモリ内で無限に増加してしまいます。複雑なアーキテクチャでは、このようなクリーニングは面倒な作業になる可能性があります。
代わりにWeakMap
に切り替えることでこれを回避できます。
//?訪問数.js visitCountMap = new WeakMap(); にしましょう。 // ウィークマップ: ユーザー => 訪問数 // 訪問数を増やします 関数 countUser(user) { let count = visitCountMap.get(user) || 0; visitCountMap.set(ユーザー, カウント + 1); }
これで、 visitsCountMap
クリーンアップする必要がなくなりました。 john
オブジェクトが到達不能になった後、 WeakMap
のキーとして以外のあらゆる手段で、そのキーによる情報とともにWeakMap
からメモリから削除されます。
もう 1 つの一般的な例はキャッシュです。関数の結果を保存 (「キャッシュ」) できるため、同じオブジェクトに対する将来の呼び出しでその結果を再利用できます。
これを実現するには、 Map
使用できます (最適なシナリオではありません)。
//?キャッシュ.js let キャッシュ = new Map(); // 計算して結果を記憶する 関数プロセス(obj) { if (!cache.has(obj)) { let result = /* */ obj の結果の計算; キャッシュ.セット(obj, 結果); 結果を返します。 } 戻りキャッシュ.get(obj); } // 次に、別のファイルで process() を使用します。 //?メイン.js let obj = {/* オブジェクト */} があるとします。 result1 = プロセス(obj); とします。 // 計算済み // ...後で、コードの別の場所から... result2 = プロセス(obj); とします。 // キャッシュから取得した結果を記憶 // ...後で、オブジェクトが必要なくなったとき: obj = null; アラート(キャッシュ.サイズ); // 1 (ああ! オブジェクトはまだキャッシュ内にあり、メモリを消費しています!)
同じオブジェクトに対してprocess(obj)
を複数回呼び出した場合、結果は初回のみ計算され、その後はその結果がcache
から取得されます。欠点は、オブジェクトが不要になったときにcache
クリーンアップする必要があることです。
Map
WeakMap
に置き換えると、この問題は解消されます。キャッシュされた結果は、オブジェクトがガベージ コレクションされた後、メモリから自動的に削除されます。
//?キャッシュ.js let キャッシュ = new WeakMap(); // 計算して結果を記憶する 関数プロセス(obj) { if (!cache.has(obj)) { let result = /* */ obj の結果を計算します。 キャッシュ.セット(obj, 結果); 結果を返します。 } 戻りキャッシュ.get(obj); } //?メイン.js let obj = {/* 何らかのオブジェクト */}; result1 = プロセス(obj); とします。 result2 = プロセス(obj); とします。 // ...後で、オブジェクトが必要なくなったとき: obj = null; // WeakMap であるため、cache.size を取得できません。 // でも 0 か、もうすぐ 0 になります // obj がガベージ コレクションされると、キャッシュされたデータも削除されます
WeakSet
同様に動作します。
これはSet
に似ていますが、オブジェクトをWeakSet
に追加するだけです (プリミティブではありません)。
オブジェクトはセット内に存在しますが、他の場所からアクセス可能です。
Set
と同様に、 add
、 has
、 delete
サポートしますが、 size
、 keys()
サポートせず、反復はサポートしません。
「弱い」ので、追加のストレージとしても機能します。ただし、任意のデータではなく、「はい/いいえ」の事実が対象です。 WeakSet
のメンバーシップは、オブジェクトに関して何かを意味する可能性があります。
たとえば、ユーザーをWeakSet
に追加して、サイトを訪問したユーザーを追跡できます。
訪問セット = 新しい WeakSet(); にします。 let john = { 名前: "ジョン" }; let ピート = { 名前: "ピート" }; メアリー = { 名前: "メアリー" }; VisitedSet.add(ジョン); // ジョンが私たちを訪ねてきました VisitedSet.add(ピート); // それからピート VisitedSet.add(ジョン); // ジョンまたまた // VisitedSet には現在 2 人のユーザーがいます // ジョンが訪問したかどうかを確認しますか? alert(visitedSet.has(john)); // 真実 // メアリーが訪問したかどうかを確認しますか? alert(visitedSet.has(mary)); // 間違い ジョン = null; // VisitedSet は自動的にクリーンアップされます
WeakMap
とWeakSet
の最も注目すべき制限は、反復がないことと、現在のコンテンツをすべて取得できないことです。これは不便に見えるかもしれませんが、 WeakMap/WeakSet
本来の仕事、つまり別の場所に保存/管理されるオブジェクトのデータの「追加」ストレージを実行することを妨げるものではありません。
WeakMap
はMap
に似たコレクションで、オブジェクトのみをキーとして許可し、他の手段でアクセスできなくなった場合は関連する値とともにオブジェクトを削除します。
WeakSet
オブジェクトのみを保存し、他の手段でアクセスできなくなるとオブジェクトを削除するSet
のようなコレクションです。
これらの主な利点は、オブジェクトへの弱い参照があるため、ガベージ コレクターによって簡単に削除できることです。
その代償として、 clear
、 size
、 keys
、 values
がサポートされないという代償が伴います。
WeakMap
とWeakSet
、「プライマリ」オブジェクト ストレージに加えて、「セカンダリ」データ構造としても使用されます。オブジェクトがプライマリ ストレージから削除されると、そのオブジェクトがWeakMap
のキーまたはWeakSet
内でのみ見つかった場合は、自動的にクリーンアップされます。
重要度: 5
一連のメッセージがあります。
メッセージ = [ {テキスト: "こんにちは"、差出人: "ジョン"}、 {テキスト: "調子はどうですか?"、差出人: "ジョン"}、 {テキスト: "また会いましょう"、差出人: "アリス"} ];
自分のコードからアクセスできますが、メッセージは他の人のコードによって管理されます。新しいメッセージはそのコードによって定期的に追加され、古いメッセージは削除されますが、それがいつ起こるかを正確に知ることはできません。
さて、メッセージが「読まれた」かどうかに関する情報を保存するには、どのデータ構造を使用できますか? 「読まれたか?」という答えを与えるのに適した構造でなければなりません。指定されたメッセージ オブジェクトの場合。
PS メッセージがmessages
から削除されると、そのメッセージは構造からも消えるはずです。
PPS メッセージ オブジェクトを変更するのではなく、プロパティを追加する必要があります。他人のコードで管理されているため、悪い結果を招く可能性があります。
既読メッセージをWeakSet
に保存しましょう。
メッセージ = [ {テキスト: "こんにちは"、差出人: "ジョン"}、 {テキスト: "調子はどうですか?"、差出人: "ジョン"}、 {テキスト: "また会いましょう"、差出人: "アリス"} ]; readMessages = new WeakSet(); にしましょう。 // 2 つのメッセージが読まれました readMessages.add(messages[0]); readMessages.add(messages[1]); // readMessages には 2 つの要素があります // ...最初のメッセージをもう一度読んでみましょう! readMessages.add(messages[0]); // readMessages にはまだ 2 つの固有の要素があります // 答え: message[0] は読まれましたか? alert("メッセージ 0 を読み取ります: " + readMessages.has(messages[0])); // 真実 メッセージ.シフト(); // 現在、readMessages には 1 つの要素があります (技術的にはメモリは後でクリーンアップされる可能性があります)
WeakSet
使用すると、メッセージのセットを保存し、その中にメッセージが存在するかどうかを簡単にチェックできます。
自動的にクリーンアップされます。トレードオフは、それを反復処理できず、そこから「すべての既読メッセージ」を直接取得できないことです。しかし、すべてのメッセージを反復処理し、セット内のメッセージをフィルタリングすることでこれを行うことができます。
もう 1 つの異なる解決策は、メッセージの読み取り後にmessage.isRead=true
のようなプロパティをメッセージに追加することです。メッセージ オブジェクトは別のコードによって管理されるため、通常は推奨されませんが、シンボリック プロパティを使用して競合を回避できます。
このような:
// シンボリック プロパティはコードだけが知っています let isRead = Symbol("isRead"); メッセージ[0][isRead] = true;
現在、サードパーティのコードはおそらく追加のプロパティを参照しません。
シンボルを使用すると問題が発生する可能性を低くできますが、アーキテクチャの観点からはWeakSet
使用する方が優れています。
重要度: 5
前のタスクと同様に、メッセージの配列があります。状況も同様です。
メッセージ = [ {テキスト: "こんにちは"、差出人: "ジョン"}、 {テキスト: "調子はどうですか?"、差出人: "ジョン"}、 {テキスト: "また会いましょう"、差出人: "アリス"} ];
ここでの質問は、「メッセージがいつ読まれたか」という情報を保存するためにどのデータ構造を提案するかです。
前のタスクでは、「はい/いいえ」という事実を保存するだけで済みました。次に、日付を保存する必要があります。日付は、メッセージがガベージ コレクションされるまでメモリ内にのみ残る必要があります。
PS 日付は、後で説明する組み込みのDate
クラスのオブジェクトとして保存できます。
日付を保存するには、 WeakMap
使用できます。
メッセージ = [ {テキスト: "こんにちは"、差出人: "ジョン"}、 {テキスト: "調子はどうですか?"、差出人: "ジョン"}、 {テキスト: "また会いましょう"、差出人: "アリス"} ]; let readMap = new WeakMap(); readMap.set(messages[0], new Date(2017, 1, 1)); // 日付オブジェクトについては後で学習します