🌀

WeakMapオブジェクトとWeakSetオブジェクトに関して

2022/02/07に公開

どうもフロントエンドエンジニアのoreoです。これまでmapオブジェクトsetオブジェクトについて整理しました。今回は、ほぼ馴染みがなかったWeakMapオブジェクトとWeakSetオブジェクトに関して、整理したいと思います。

WeakMapとは?

WeakMapオブジェクトとは、キーとバリューを保持するコレクションです。Mapオブジェクトに似ていますが、①キーは必ずオブジェクトでなければならない、②キーに指定したオブジェクトの参照は弱参照(後述)となる、③反復処理できないなどの違いがあります。

WeakMapの主なメソッド

  • new WeakMap()
    • WeakMap オブジェクトを作成できる。
  • weakMap.set(key, value)
    • 第1引数にキー、第2引数にバリューをそれぞれ渡して設定することができる。
  • weakMap.get(key)
    • 引数として渡したキーのバリューを取得できる。
  • weakMap.delete(key)
    • 引数として渡したキーのバリューを削除できます。
  • weakMap.has(key)
    • 引数として渡したキーが存在するかをbooleanで返します。
const test = {};

const  wm = new WeakMap();
wm.set(test, "hoge");     //キー:test、バリュー:"hoge"を設定

console.log(wm.has(test));  //true
console.log(wm.get(test));  //hoge

wm.delete(test);        //キー:test、バリュー:"hoge"を削除
console.log(wm.has(test));  //false

弱参照について

そもそもJavaScriptのobjectでは、objectの実体への参照がなくなれば、ガベレージコレクションにより、実体が削除され、使用していたメモリが開放されます(ex1参照)。

※ガベージコレクション

オブジェクトや文字列などの実体が、どこからも参照されなくなった場合に、自動的にそれらが使用していたメモリ領域を開放してくれる仕組み。

//ex1

//①objの宣言
//オブジェクトの実体{name:"hoge"}がメモリ上に配置される、objは、その実体への参照を保持している。
let obj = {name:"hoge"};
console.log(obj)  //{ name: 'hoge' }

//②objにnullを代入する
//変数obj→実体{name:"hoge"}への参照はなくなる。
obj = null;

//③実体{name:"hoge"}への参照は存在しないので、ガベレージコレクションが実施される。
console.log(obj)  //null。

arrayなどのオブジェクト型を用いて、実体への参照を保持した場合、その参照が保持され続けると、ガベレージコレクションは実施されず、メモリリーク(メモリ領域が解放されず、利用可能なメモリ領域が減少する)が発生する可能性があります(ex2参照)。

//ex2

//①objの宣言
//オブジェクトの実体{name:"hoge"}がメモリ上に配置される、objは、その実体への参照を保持している。
let obj = {name:"hoge"};

//②arrayの宣言
//arrayに、実体{name:"hoge"}への参照が保持される
let array = [obj]
console.log(array)  //[ { name: 'hoge' } ]。

//③objにnullを代入する
//obj→実体{name:"hoge"}への参照はなくなる。
obj = null;

//④しかし、array→実体{name:"hoge"}への参照が存在するので、ガベレージコレクションは実施されない。
console.log(array)  //[ { name: 'hoge' } ] 。arrayから実体{name:"hoge"}への参照が存在している

一方、WeakMapオブジェクトのキーに指定したオブジェクトの実体への参照は、その参照以外の参照が存在しない場合、ガベレージコレクションが実施されます。これを、弱参照と言います(ex3)。

//ex3

//①objの宣言
//オブジェクトの実体{name:"hoge"}がメモリ上に配置される、objは、その実体への参照を保持している。
let obj = {name:"hoge"};

//②mapオブジェクトの宣言
//weakMapのキーに実体{name:"hoge"}への弱参照が保持される
let weakMap = new WeakMap();
weakMap.set(obj, "fuga");
console.log(weakMap.has(obj)); //true

//③objにnullを代入する
//obj→実体{name:"hoge"}への参照はなくなる。
obj = null;

//④weakMap→実体{name:"hoge"}への参照は弱参照なので、ガベレージコレクションが実施される。
console.log(weakMap.has(obj));  //false

WeakMapはどんな時に使えるのか?

プライベート変数定義などに使われる模様です。

具体的には、下記のようなDogクラスを定義すると、変数_nameにクラスの外部からアクセスできます。

//プライベート変数使わない時
class Dog {
    constructor(name){
        this._name = name;
    }
    bow(){
        console.log(`${this._name}が吠えた`);
    }
} 

const shiba = new Dog('柴犬');
shiba.bow();         //「柴犬が吠えた」と出力。
console.log(shiba._name)  //「柴犬」と出力。外部からアクセスできてしまう。
console.log(shiba)      //「Dog { _name: '柴犬' }」と出力。

そこで、WeakMapを使用すると、変数nameを、プライベート変数にし、外部からアクセスできないようにできます。

const wm = new WeakMap();

class Dog {
    constructor(name){
        wm.set(this,{name});
    }
    bow(){
        console.log(`${wm.get(this).name}が吠えた`);
    }
} 

const shiba = new Dog('柴犬');
shiba.bow();         //「柴犬が吠えた」と出力。
console.log(shiba.name)   //undefiend。アクセスできない。
console.log(shiba)      //「Dog {}」と出力。

WeakSetとは?

WeakSetオブジェクトとは、重複のない一意のバリューを保持するコレクションです。

Setオブジェクトに似ていますが、①バリューは必ずオブジェクトでなければならない、②バリューに指定したオブジェクトへの参照は弱参照(詳細は上記参照)となる、③反復処理できないなどの違いがあります。

WeakSetの主なメソッド

  • new WeakSet()
    • WeakSet オブジェクトを作成できる。
  • WeakSet.add(value)
    • 引数として渡したバリューを設定することができる。
  • WeakSet.delete(key)
    • 引数として渡したリューを削除できます。
  • WeakSet.has(key)
    • 引数として渡したキーが存在するかをbooleanで返します。
const test = {};

const ws = new WeakSet();

ws.add(test);         //バリューtestを追加
console.log(ws.has(test)); //true

ws.delete(test);       //バリューtestを削除
console.log(ws.has(test)); //false

WeakSetはどんな時に使えるのか?

WeakSetオブジェクトを用いて、参照済みのオブジェクトをチェックすることで、循環参照(オブジェクトの参照がループしてしまう現象)を検知できる模様です。

👇こちら参考

オブジェクトが循環参照しているかを検証する方法(ガチめ) - Qiita

最後に

Vue.jsのソースコードを読んでいた時に、WeakMapオブジェクトを初めて知り、今回調べてみました。弱参照などは知らなかった概念なので、大変勉強になりました。OSSを読む際に、これらがどのような機能を担っているのか、意識しながら読み、理解を深めて行きたいです!

参考

メモリ管理 - JavaScript | MDN

WeakMap - JavaScript | MDN

キー付きコレクション - JavaScript | MDN

WeakSet - JavaScript | MDN

Discussion