【JavaScript】 WeakMapでオブジェクトとデータの関連付けをおこなう
更新日:2021/01/19
JavaScriptにはWeakMapというオブジェクトが標準で用意されています。
これを使用しなくても他の方法で間に合ってしまうので、知らない人が多いと思います。
そこで今回はWeakMapについてお伝えします。
WeakMapとは
WeakMapはオブジェクトをキーとして、データの格納や取得をおこなうJavaScript標準のオブジェクトです。
WeakMap内のオブジェクトキーは重複できません。
同様のオブジェクトにMapがあり、機能はほぼ同じです。
ただし、WeakMapはメモリリソースを「リーク」させないような機能が備わっています。
WeakMapとMapの違いについては後述します。
WeakMapの使い方
WeakMapの使い方を簡単に解説します。
WeakMapの使用例
// ①オブジェクトキーを用意
const key1 = {};
// ②WeakMapインスタンスを作成
const wmap = new WeakMap();
// ③キーと値を登録/値を取得
wmap.set( key1 , 1 );
console.log( wmap.get( key1 ) ); // 1
// ④キーが登録されているか確認
console.log( wmap.has( key1 ) ); // true
// ⑤キーを削除
wmap.delete( key1 );
console.log( wmap.has( key1 ) ); // false
①オブジェクトキーを用意
上の例では、key1をオブジェクトキーとして使用しています。
②WeakMapインスタンスを作成
WeakMapを使用するには、まずWeakMapコンストラクターからインスタンスを作成します(②)
上の例ではコンストラクターに引数を与えていませんが、次のような方法で初期値を指定できます。
new WeakMap( [ [ キー , 値 ] , [ キー , 値 ] ] );
例
const key2 = {};
const key3 = {};
const wmap = new WeakMap( [ [key2,2] ,[key3,3] ] );
console.log( wmap.get( key2 ) ); // 2
console.log( wmap.get( key3 ) ); // 3
次のようにキーを直接指定できますが、値を取り出すには変数が必要なため、あまり意味がありません。
// 初期値として与えたデータを取り出すことができない。
const wmap = new WeakMap( [ [ {} , 2 ] ,[ {} , 3 ] ] );
理由は次項オブジェクトをキーにするとはで説明します。
③キーと値を登録/値を取得
WeakMapのsetメソッドを使用して、オブジェクトキーと値を登録します。
第一引数にオブジェクトを、第二引数に値を指定します。
WeakMap.set( オブジェクトキー , 値 )
登録した値はgetメソッドを使用することで、取り出すことができます。
WeakMap.get( オブジェクトキー )
また、同一キーでsetメソッドを呼び出すと、登録されている値が上書きされます。
wmap.set( key1 , 1 );
wmap.set( key1 , 10 );
console.log( wmap.get( key1 ) ); // 10 ← 1から10に上書きされた
なお、コンストラクターと同様に、次のコードは意味がありません。
// データを取り出すことができない。
wmap.set( {} , 1 );
④キーが登録されているか確認
hasメソッドを使用すると、キーが登録されているか確認できます。
WeakMap.has( オブジェクトキー )
返り値がtrueなら登録済み、falseなら未登録です。
⑤キーを削除
deleteメソッドを使用すると、キーを削除できます。
実体を削除しているのではないことに注意しましょう。
詳しくは次項オブジェクトをキーにするとはで説明します。
オブジェクトをキーにするとは
オブジェクトをキーとして扱うという意味は少しわかりにくいですね。
簡単に解説をしておきます。
まずは実体と変数の関係からです。
次のような空のオブジェクトを作成してみます。
const obj = {};
上のコードが実行されるとオブジェクトの実体が作成され、ラベルのようなものがつけられます。
このラベルは重複しません。
しかしラベルのようなものはJavaScriptの内部のみで使用され外部から見ることができません。
そのため実際にどんな値かはわかりません。
文字かもしれませんし数値かもしれません。
とりあえずここでは"オブジェクト001"とします。
つけられたラベルは、変数の値としてコピーされます。
変数はこの値を逆にたどって、実体を取得します。
次に変数objをobj2に代入します。
const obj2 = obj;
objの値であるラベルのようなものが、obj2にコピーされます。
その結果、objとobj2は同じ実体を参照することになります。
では次に、WeakMapのsetメソッドで、objをオブジェクトキーとして指定してみます。
wmap.set( obj , 値 )
まずobjの値であるラベルのようなものが、オブジェクトを指しているかどうかチェックされます。
オブジェクトだったなら、ラベルのようなものがオブジェクトキーとして使用されます。
つまり変数ではなく、変数が持っている値がキーとして保持されます。
次にgetメソッドで値を取り出してみます。
wmap.get( obj )
objの"オブジェクト001"という値と、WeakMap内のオブジェクトキーで一致するものが検索されます。
一致するものがあったら、対応する値が返ります。
obj2も保持している値が"オブジェクト001"なので、objを指定したときと同じ値が返ってきます。
wmap.get( obj2 )
しかしオブジェクトの実体を参照する変数が全て削除されると、オブジェクトキーを検索する手段がなくなります。
wmap.get( 指定できる変数がない!! )
そのため、その時点でデータを取得することができなくなります。
なお、オブジェクトキーと共に登録されるデータも、データの実体ではなく値への参照がセットされています。
deleteメソッドで削除されるのは、それらの参照値であり実体ではないことに注意しましょう。
具体的な使用目的
ここまでの説明では、WeakMapをどのような目的で使用すればいいのかわかりませんね。
WeakMapはオブジェクトとデータを関連付けることができますが、わざわざこのオブジェクトを使用しなくても、プロパティを使用する方が手っ取り早いです。
オブジェクトとデータの関連付け
■WeakMap使用
const data = 1000;
const obj = {};
const wmap = new WeakMap();
wmap.set( obj , data );
■プロパティを使用
obj.data = data;
オブジェクトにプロパティを追加したくない
しかし場合によっては、オブジェクトにプロパティを追加したくないというケースがあります。
次のコードは、【JavaScript】 sort()メソッドを使用した配列並び替えを極めるという記事で使用した例を、少し変更したものです。
WeakMap未使用の場合
WeakMap未使用
// カンマ区切りデータの要素数をカウント
const csvCount = t => t.split(",").length;
// 対象データ: カンマで分割した要素数でソートしたい
const data = [
{csv:"犬,ネコ,猿,コアラ"},
{csv:"羊,ヤギ"},
{csv:"ネズミ,牛,いのしし"}
];
// ソート処理
const dataSort = d => {
const sym = Symbol(); // 一時プロパティ用シンボル値作成
data.forEach( e => e[sym] = csvCount(e.csv)); // ソート用データを一時プロパティにセット
d.sort( (a , b) => a[sym] - b[sym] ); // ソート実行
data.forEach( e => delete e[sym] ); // 一時プロパティを削除
return d;
};
console.log( dataSort( data ) );
// 結果: [ { csv: "羊,ヤギ" }
// { csv: "ネズミ,牛,いのしし" }
// { csv: "犬,ネコ,猿,コアラ" } ]
上の例はカンマ区切りの文字列を、分割後の要素数でソートしています。
例えば"犬,ネコ,猿,コアラ"なら、4がソート対象の値となります。
要素数を計算する関数はcsvCountなので、次のようにsortメソッドを呼び出せば並び替えができます。
d.sort( (a , b) => csvCount(a.csv) - csvCount(b.csv) );
しかしsortメソッドは同じオブジェクトを複数回呼び出します。その分csvCountが同じ処理を何度も繰り返すことになります。
非常に効率が悪いので、ソート前にcsvCountの結果をプロパティにセットしています。
プロパティにシンボルを使用しているのは、既存プロパティの上書きの可能性を排除するためです。
シンボルについては、こちらの記事を見てください。
【JavaScript】 シンボル(Symbol)とは?使い方を解説します
ここである問題が発生します。
dataSort関数を他者が使用したとき、”関数にオブジェクトを引数として渡したら知らないプロパティが増えていた”と思われたら、何か余計なことをやっているかもしれないと、不審な関数と判断されてしまいます。
なので最後はしっかりと、一時プロパティを削除しています。
WeakMap使用の場合
上の例は好悪を除いて特に問題はないのですが、やっぱりプロパティを追加するのはイヤというのなら、WeakMapを使用します。
WeakMap使用
const dataSort = d => {
const wmap = new WeakMap();
data.forEach( e => wmap.set( e, csvCount(e.csv) ));
return d.sort( (a , b) => wmap.get(a) - wmap.get(b) );
};
console.log( dataSort( data ) );
// 結果: [ { csv: "羊,ヤギ" }
// { csv: "ネズミ,牛,いのしし" }
// { csv: "犬,ネコ,猿,コアラ" } ]
引数として受け取ったオブジェクトを改変することなく、目的を達成することができました。
他の人がコードを見て、「こんなの非推奨だ!!」と言われることもありません。
ちなみにWeakMapで登録したキーと値は、弱い参照という特徴を持っています。
詳しくは後述しますが、この特徴によりWeakMapのdeleteメソッドを呼び出す必要がありません。
そのため、前のバージョンよりスッキリとしたコードになりました。
実データが把握外のタイミングで削除される場合
オブジェクトキーやデータがプログラムコードの制御外で削除される可能性がある場合は、WeakMapを使用したほうがよいケースもあります。
例えば次のように、DOM要素をキーとして使用するケースです。
const li = document.getElementsByTagName("li");
const wmap = new WeakMap();
for (let i = 0; i < li.length; i++){
wmap.set( li[i] , i );
}
DOM要素は同じhtmlから呼び出された別のスクリプトで削除される可能性があります。
その際、参照がプログラムコードで保持されていると、要素の実データが削除されません。
しかしWeakMapは弱い参照という特徴を持っているので、実データが削除されます。
この解説ではよくわからないと思うので、次項の弱い参照を読んでください。
WeakMapとMapの違い
WeakMapのWeakはウィークポイントのウィークで、弱いという意味です。
つまりWeakMapオブジェクトは、弱いMapオブジェクトという意味です。
ここでは何が弱いのかを含めて、弱いWeakMapの違いをお伝えします。
弱い参照
何が弱いのか結論から言うと、実データと内部変数との結びつきが弱いです。
この結びつきは参照という言葉であらわされることがあり、弱い参照と言い直すことができます。
弱い参照とは何なのかを解説するには、JavaScriptの実データの削除の仕組みを知っておく必要があります。
実データの削除の仕組み(ガベージコレクション)
オブジェクトをキーにするとはで、変数は実データに割り振ったラベルのようなものを保持していると説明しました。
変数に他のデータへの参照が代入されたり、変数が削除されたりして全ての参照が失われると、それ以降は実データへの参照を変数に割り振る方法がありません。
つまり、実データに二度とアクセスできなくなります。
JavaScriptはこの状態を検知して、実データを削除します。
実データの必要性を判断し削除する仕組みは、ガベージコレクションと呼ばれています。
実際はもっと複雑なことをおこなっています。
なおガベージコレクションのアルゴリズムはJavaScriptの仕様に含まれていません。
そのためブラウザ等にJavaScriptを組み込むベンダーの判断で設計されています。
WeakMapと弱い参照
次にWeakMapを使用したケースを考えてみます。
オブジェクトキーとして実データを指定した場合、WeakMapの内部変数に実データへの参照を保持しています。
この状態で他の変数が削除されても、内部変数は残ります。
WeakMapは他の変数がないとデータを引き出すことができません。
変数も存在しないためオブジェクトの実体にアクセスすることが不可能になります。
そのため実質的に、オブジェクトの実体は不必要なデータです。
しかしWeakMapの内部変数から参照されているため、オブジェクトの実体は削除されません。
そこで弱い参照という概念が考案されました。
参照と異なり、弱い参照はJavaScriptのデータ削除のチェック対象から除外されます。
これによりWeakMap内からの参照が残っていても、実データが削除されます。
Mapオブジェクトは通常の参照です。
そのため他の変数から全ての参照が失われても、実データは削除されません。
参照を削除するにはdeleteメソッドを呼び出す必要があります。
WeakMapは登録したデータを列挙できない
MapオブジェクトはforEachメソッドで、登録されているキーと値を列挙できます。
const obj1 = {};
const obj2 = {};
const map = new Map( [ [obj1,1],[obj2,2] ]);
map.forEach(
(value , key )=>console.log( key , value )
);
しかしWeakMapオブジェクトは、列挙する手段が用意されていません。
理由は、上記のガベージコレクションにあります。
ガベージコレクションは変数が不必要になったら即座に実行されるわけではありません。
ある程度の期間をおいて、一定のタイミングで実行されます。
このタイミングはプログラムコードで把握することはできませんし、使用しているブラウザ等の環境が異なればタイミングも変わってきます。
そのためWeakMapのデータを列挙しようとすると、削除予定のデータがタイミングによって取得できたり取得できなかったりします。
不具合の原因になるだけでなく、ブラウザが異なると再現しないなど、開発者泣かせの機能となってしまいます。
このような理由から、WeakMapオブジェクトは登録されているデータを列挙できないようになっています。
実際にはJavaSctriptの仕様書に理由は明記されていません。
そのため上に書いたことは想像です。
他に何か大きな理由があるかもしれません。
更新日:2021/01/19
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。