ES6 WeakMapの実際の用途は何ですか?


397

WeakMapECMAScript 6で導入されたデータ構造の実際の用途は何ですか?

弱マップのキーは、その対応する値への強い参照を作成するので、弱マップに挿入された値は、あろうことを確実に決して限り、そのキーがまだ生きているように消滅しない、それは、メモテーブルのために使用することができませんキャッシュなど、通常は弱い参照、弱い値のマップなどを使用します。

これは私には思えます:

weakmap.set(key, value);

...これは単なる迂回的な方法です:

key.value = value;

どの具体的なユースケースが欠けていますか?




35
実際の使用例:DOMノードのカスタムデータを保存します。
Felix Kling、2015

弱参照について言及するすべてのユースケースも非常に重要です。彼らは非決定論を導入するので、それらを言語に追加するのははるかに難しいです。Mark Millerや他の人は弱い参照について多くの作業を行っており、最終的にそれらが来ると思います。最終的に
Benjamin Gruenbaum 2015

2
WeakMapsを使用してメモリリークを検出できます:stevehanov.ca/blog/
id=

回答:


513

基本的に

WeakMapsは、ガベージコレクションを妨げずにオブジェクトを外部から拡張する方法を提供します。オブジェクトを拡張したいが、それがシールされているために拡張できない場合-または外部ソースから-WeakMapを適用できます。

A WeakMapマップ(辞書)であるキーが弱い-へのすべての参照場合、であるキーは -失われた値への参照がなくなっされる値は、ガベージを収集することができます。最初に例を通してこれを示し、次に少し説明し、最後に実際の使用で終了しましょう。

特定のオブジェクトを提供するAPIを使用しているとします。

var obj = getObjectFromLibrary();

これで、オブジェクトを使用するメソッドができました。

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

特定のオブジェクトを使用してメソッドが呼び出された回数を追跡し、それがN回を超えて発生したかどうかを報告したい。初心者はマップを使用すると思います:

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);
}

そして、メモリリークはなくなりました。

ユースケース

そうでなければメモリリークを引き起こし、WeakMaps によって有効になるいくつかのユースケースは次のとおりです。

  • 特定のオブジェクトに関するプライベートデータを保持し、マップへの参照を持つユーザーにのみアクセスを許可します。プライベートシンボルの提案では、よりアドホックなアプローチが採用されていますが、それは今から長い時間です。
  • 変更したりオーバーヘッドを発生させたりすることなく、ライブラリオブジェクトに関するデータを保持する。
  • 同じタイプのオブジェクトに対してJSエンジンが使用する非表示クラスで問題が発生しないように、タイプのオブジェクトが多数存在する少数のオブジェクトセットに関するデータを保持します。
  • DOMノードなどのホストオブジェクトに関するデータをブラウザーに保持します。
  • 外部からオブジェクトに機能を追加する(他の回答のイベントエミッターの例のように)。

実際の用途を見てみましょう

外部からオブジェクトを拡張するために使用できます。Node.jsの実世界からの実用的な(適応された、実際のような-要点を言う)例を挙げましょう。

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

ここで、明らかな理由でネイティブオブジェクトにプロパティを追加したくないので、行き詰まっています。promiseへの参照を維持すると、ガベージコレクションが発生しないため、メモリリークが発生しています。参照を維持しないと、個々のプロミスに関する追加情報を保存できません。PromiseのIDの保存を伴うスキームは、本質的にそれへの参照が必要であることを意味します。

弱いマップに入る

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

これは、promiseが与えられれば、それについての状態を保存できることを意味します-そして、そのオブジェクトはガベージコレクションされます 後で、オブジェクトへの参照を取得した場合、そのオブジェクトに関連する状態があるかどうかを確認して報告できます。

これは、Petka Antonovによって、次のように未処理の拒否フックを実装するために使用されました

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
こんにちは!サンプルコードのどの部分でメモリリークが発生するか教えていただけますか?
ltamajs

15
@ ltamajs4確かに、useObja Mapではなくa WeakMapを使用する例では、渡されたオブジェクトをマップキーとして使用します。オブジェクトはマップから削除されることはないので(いつ実行するかわからないため)、常にオブジェクトへの参照があり、ガベージコレクションを実行することはできません。WeakMapの例では、オブジェクトへの他のすべての参照がなくなるとすぐに、オブジェクトをからクリアできますWeakMap。それでもわからない場合はお知らせください
Benjamin Gruenbaum

@Benjamin、メモリに依存するキャッシュの必要性とdata_objectタプルの必要性を区別する必要があります。これら2つの個別の要件を混同しないでください。あなたのcalled例はjsfiddle.net/f2efbm7zを使用してよりよく書かれており、弱いマップの使用を示していません。実際、それは合計6通りの方法で書くことができます。
パセリエ2017

基本的に、ウィークマップの目的はメモリ依存のキャッシュです。外部からオブジェクトを拡張するために使用することはできますが、これは実際にはお粗末なハックであり、適切な目的ではありません
パセリエ2017

1
プロミスとプロミスが処理/拒否された回数の間にリンクを保持したい場合は、1)記号を使用します。p[key_symbol] = data。または2)固有の命名。p.__key = data。または3)プライベートスコープ。(()=>{let data; p.Key = _=>data=_;})()。または4) 1または2または3のプロキシ。または5) Promiseクラスを1または2または3で 置換/拡張。または6) Promiseクラスを必要なメンバーのタプルで置換/拡張。—いずれの場合も、メモリ依存のキャッシュが必要でない限り、弱いマップは必要ありません
パセリエ2017

48

この答えは偏りがあり、現実のシナリオでは使用できないようです。そのままお読みください。実験以外のオプションではありません。

ユースケースは、それをリスナーの辞書として使用することかもしれません、私はそれをした同僚を持っています。どのリスナーもこの方法で直接対象とされるため、非常に役立ちます。さようならlistener.on

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


次のものを読む前に

私は今、私の強調が問題に取り組む最善の方法ではないことに気づきました。ベンジャミン・グルエンバウムが指摘したように(彼の答えが私のものより上にない場合は、彼の答えを確認してください:p)、この問題は通常の方法Mapでは解決できなかったでしょう。リークしていたので、その主な長所はWeakMap、参照を保持していなければ、ガベージコレクションを妨げないことです。


これが私の同僚の実際のコードです(共有しくれてありがとう)

ここ完全な情報源、それは私が上で話したリスナー管理についてです(あなたは仕様を見ることもできます)

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);
    }
}

2
あなたがこれをどのように使うか、私にはよくわかりません。参照されなくなったときに、それにバインドされたイベントとともにオブザーバブルが折りたたまれる可能性があります。私がよく抱える問題は、オブザーバーが参照されなくなったときです。ここでの解決策は問題の半分しか解決しなかったと思います。WeakMapではオブザーバーの問題を反復できないため、解決できないと思います。
jgmjgm 2016

1
ダブルバッファリングイベントリスナーは他の言語では高速かもしれませんが、この場合は単純に難解で低速です。それは私の3セントです。
ジャックギフィン

@axelduch、うわー、このリスナーハンドルの神話は、JavaScriptコミュニティーまでずっと売られており、40の賛成票を得ています。この回答が完全に間違っている理由を理解するにstackoverflow.com
a / 156618/632951の

1
@Pacerierが回答を更新しました。フィードバックをありがとう
axelduch

1
@axelduch、そう、そこからの参照もあります。
パセリエ2017

18

WeakMap カプセル化と情報隠蔽に適しています

WeakMapES6以降でのみ使用できます。A WeakMapはキーと値のペアのコレクションであり、キーはオブジェクトでなければなりません。次の例ではWeakMap、2つのアイテムでを作成します。

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、マップ内のキーへの弱い参照を保持しているという事実です。弱い参照は、オブジェクトが破棄された場合、ガベージコレクターがからエントリ全体を削除し、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;
    }());
})()

4
「weakmapはカプセル化と情報の隠蔽にうまく機能します」について。できるからといって、そうする必要があるわけではありません。Javascriptには、weakmapが発明される前でも、カプセル化と情報非表示を行うデフォルトの方法があります。現在のように、それを行うには文字通り6つの方法があります。ウィークマップを使用してカプセル化を行うことは、醜いフェイスパームです。
パセリエ2017

12

𝗠𝗲𝘁𝗮𝗱𝗮𝘁𝗮

ウィークマップを使用すると、ガベージコレクションを妨害したり、コードに同僚を怒らせたりすることなく、DOM要素に関するメタデータを格納できます。たとえば、それらを使用して、Webページ内のすべての要素に数値インデックスを付けることができます。

𝗪𝗲𝗮𝗸𝗠𝗮𝗽𝘀𝗼𝗿𝗪𝗲𝗮𝗸𝗦𝗲𝘁𝘀:

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[(i * i) % 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"
  )   // }
);

𝗪𝗲𝗮𝗸𝗠𝗮𝗽𝘀𝗮𝗻𝗱𝗪𝗲𝗮𝗸𝗦𝗲𝘁𝘀:

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[(i * i) % 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"
  )   // }
);

𝗧𝗵𝗲𝗗𝗶𝗳𝗳𝗲𝗿𝗲𝗻𝗰𝗲

weakmapバージョンが長いという事実を除けば、違いはごくわずかに見えるかもしれませんが、上記の2つのコードには大きな違いがあります。弱いマップなしの最初のコードスニペットでは、コードの一部がDOM要素間のあらゆる方法で参照を格納します。これにより、DOM要素がガベージコレクションされなくなります。(i * i) % len誰も使用しない奇妙なボールのように見えるかもしれませんが、もう一度考えてみてください。多くのプロダクションコードには、ドキュメント全体に行き渡るDOM参照があります。さて、2番目のコードでは、要素へのすべての参照が弱いため、ノードを削除すると、ブラウザーはノードが使用されていない(コードから到達できない)と判断できます。したがって、メモリから削除します。メモリ使用量とメモリアンカー(未使用の要素がメモリに保持されているコードの最初のスニペットのようなもの)を気にする必要がある理由は、メモリ使用量が多いほど、ブラウザのGC試行が多くなるためです(メモリを解放するためにブラウザーのクラッシュを回避する)とは、ブラウジングエクスペリエンスが遅くなり、場合によってはブラウザーがクラッシュすることを意味します。

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

〜ハッピーコーディング!


1
明確な説明をありがとう。一例はどんな言葉よりも価値があります。
newguy 2017

@lolzery、Re " これにより、DOM要素がガベージコレクションされないようにします "、必要なのはnull設定elementsするだけです。これで、GCされます。 Re「ドキュメント全体でバウンドするDOM参照」は、まったく問題ではありません。メインリンクelementsがなくなると、すべての循環参照がGCされます。要素が必要のない要素への参照を保持している場合は、コードを修正し、使用が終わったらrefをnull設定します。GCされます。 ウィークマップは必要ありません
パセリエ2017

2
@Pacerierは熱狂的なフィードバックに感謝しますが、elementsnullに設定すると、ブラウザは最初のスニペットの状況で要素をGCできなくなります。これは、要素にカスタムプロパティを設定しても、それらの要素を引き続き取得でき、それらのカスタムプロパティに引き続きアクセスできるため、これらの要素がGCされないようにするためです。それを金属リングのチェーンのように考えてください。チェーン内の少なくとも1つのリンクにアクセスできるので、チェーン内のそのリンクを保持できるため、アイテムのチェーン全体が奈落の底に落ちるのを防ぐことができます。
ジャックギフィン2017

1
varsという名前のdunderを使用したプロダクションコードで嘔吐
Barbu Barbu

10

WeakMap不変オブジェクトをパラメーターとして受け取る関数を、心配することなくメモ化するために使用します。

メモ化は、「値を計算した後、再度計算する必要がないようにキャッシュする」という空想的な言い方です。

次に例を示します。

注意すべきいくつかの点:

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

1
これは、メモ化キャッシュがobj / functionの存続期間を通じて永続的ではなく、メモリに依存することを意図している限り、weakmapの有効な使用法です。「メモ化キャッシュ」がobj / functionの存続期間中持続することを意図している場合、weakmapは間違った選択です。代わりに6つのデフォルトのjavascriptカプセル化手法のいずれかを使用してください 。
パセリエ2017

3

WeakMapsのこのシンプルな機能ベースのユースケース/例があります。

ユーザーのコレクションを管理する

私はで始まったUserその特性が含まれ、オブジェクトfullnameusernameagegenderと呼ばれる方法printの他のプロパティの人間が読める要約を出力します。

/**
Basic User Object with common properties.
*/
function User(username, fullname, age, gender) {
    this.username = username;
    this.fullname = fullname;
    this.age = age;
    this.gender = gender;
    this.print = () => console.log(`${this.fullname} is a ${age} year old ${gender}`);
}

次に、というusersキーでマップされた複数のユーザーのコレクションを保持するために呼び出されるMapを追加しましたusername

/**
Collection of Users, keyed by username.
*/
var users = new Map();

コレクションの追加には、ユーザーを追加、取得、削除するためのヘルパー関数と、完全を期すためにすべてのユーザーを印刷するための関数も必要でした。

/**
Creates an User Object and adds it to the users Collection.
*/
var addUser = (username, fullname, age, gender) => {
    let an_user = new User(username, fullname, age, gender);
    users.set(username, an_user);
}

/**
Returns an User Object associated with the given username in the Collection.
*/
var getUser = (username) => {
    return users.get(username);
}

/**
Deletes an User Object associated with the given username in the Collection.
*/
var deleteUser = (username) => {
    users.delete(username);
}

/**
Prints summary of all the User Objects in the Collection.
*/
var printUsers = () => {
    users.forEach((user) => {
        user.print();
    });
}

上記のすべてのコードがNodeJSなどで実行されている場合、usersプロセス全体でマップのみがユーザーオブジェクトへの参照を持ちます。個々のユーザーオブジェクトへの他の参照はありません。

このコードをインタラクティブなNodeJSシェルで実行します。例として、4人のユーザーを追加して印刷します。 ユーザーの追加と印刷

既存のコードを変更せずに、ユーザーに情報を追加します

ここで、各ユーザーのソーシャルメディアプラットフォーム(SMP)リンクをユーザーオブジェクトと共に追跡する必要がある新しい機能が必要であるとしましょう。

ここで重要なのは、この機能は既存のコードへの介入を最小限に抑えて実装する必要があることです。

これは、次の方法でWeakMapsで可能です。

Twitter、Facebook、LinkedInの3つの個別のWeakMapを追加します。

/*
WeakMaps for Social Media Platforms (SMPs).
Could be replaced by a single Map which can grow
dynamically based on different SMP names . . . anyway...
*/
var sm_platform_twitter = new WeakMap();
var sm_platform_facebook = new WeakMap();
var sm_platform_linkedin = new WeakMap();

ヘルパー関数getSMPWeakMapが追加され、指定されたSMP名に関連付けられたWeakMapを返すだけです。

/**
Returns the WeakMap for the given SMP.
*/
var getSMPWeakMap = (sm_platform) => {
    if(sm_platform == "Twitter") {
        return sm_platform_twitter;
    }
    else if(sm_platform == "Facebook") {
        return sm_platform_facebook;
    }
    else if(sm_platform == "LinkedIn") {
        return sm_platform_linkedin;
    }
    return undefined;
}

指定されたSMP WeakMapにユーザーのSMPリンクを追加する関数。

/**
Adds a SMP link associated with a given User. The User must be already added to the Collection.
*/
var addUserSocialMediaLink = (username, sm_platform, sm_link) => {
    let user = getUser(username);
    let sm_platform_weakmap = getSMPWeakMap(sm_platform);
    if(user && sm_platform_weakmap) {
        sm_platform_weakmap.set(user, sm_link);
    }
}

指定されたSMPに存在するユーザーのみを印刷する機能。

/**
Prints the User's fullname and corresponding SMP link of only those Users which are on the given SMP.
*/
var printSMPUsers = (sm_platform) => {
    let sm_platform_weakmap = getSMPWeakMap(sm_platform);
    console.log(`Users of ${sm_platform}:`)
    users.forEach((user)=>{
        if(sm_platform_weakmap.has(user)) {
            console.log(`\t${user.fullname} : ${sm_platform_weakmap.get(user)}`)
        }
    });
}

ユーザーのSMPリンクを追加できるようになりました。各ユーザーが複数のSMPにリンクを持つ可能性もあります。

...前の例を続けて、ユーザーにSMPリンクを追加し、ユーザーBillとSarahに複数のリンクを追加して、各SMPのリンクを個別に印刷します。 SMPリンクをユーザーに追加して表示する

次にusers、を呼び出して、ユーザーがマップから削除されたとしdeleteUserます。これにより、ユーザーオブジェクトへの参照のみが削除されます。これはまた、ユーザーオブジェクトなしではSMPリンクのいずれにもアクセスできないため、SMPリンクのいずれか/すべて(ガベージコレクションによる)からSMPリンクをクリアします。

...例を続けて、私はユーザーBillを削除してから、ユーザーが関連付けられたSMPのリンクを出力します。

ユーザーBillをマップから削除すると、SMPリンクも削除されます

SMPリンクを個別に削除するために追加のコードを追加する必要はなく、この機能が変更される前の既存のコードも変更されません。

WeakMapsの有無にかかわらず、この機能を追加する他の方法がある場合は、コメントしてください。


_____nice______
Aleks
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.