uhyohyo.net

JavaScript初級者から中級者になろう

十六章第一回 WeakMapとWeakSet

このページの最終更新日:

第十六章では、ES2015の解説をします。これはその名の通り、2015年に標準化された新しいECMAScript仕様を指します。ES2015は今までに説明したES5までと比べても新しい機能、新しい文法などを備えています。なお、ECMAScriptというのはJavaScriptの言語仕様の名前です。なぜ素直にJavaScriptと言わないのかは歴史的経緯とかが色々あるのです。(このあたりのことは十六章第二十二回で説明する予定です。)

つまりざっくり言えば、ES2015というのは最近JavaScriptに追加された新しい機能とか文法のことです。2015というだけあってこれはなかなかに近代的な文法などが含まれています。今どきはこれくらい分かっていないと中級者とは言えないでしょう。

なお、ES2015は、ES5の次のバージョンとなので昔はES6と呼ばれていましたが、のちのちECMAScriptは年に1回バージョンアップされることになり、年の名前で呼ぶことになりました。そのため、ES2015のさらに次のバージョンとしてES2016ES2017、ES2018……と続いていくことになります。

この章ではES2015から始まり、ES2017までの機能を紹介します。ただ、ES2015というのはとても大規模なバージョンアップでしたが、ES2016, ES2017はそれに比べると小規模です。なので、実質ほとんどES2015の機能であると思っていただいて構いません。

なお、ES2017までである理由は本記事の執筆時点で最新のバージョンがES2017だからです。

ただし、ES2015を使う際はブラウザのサポートには注意してください。現在のブラウザの多くはES2015にほぼ完璧に対応しており、ES2015の機能を用いたプログラムでも正しく動作します。しかし、IE11は例外です。IEはマイクロソフトが開発していたInternet Explorerの最終バージョンで、これはES2015にほとんど対応していません。よって、IE11で動作してほしいページにはES2015を使うことができません。(それでもES2015を使いたい場合はトランスパイラでES5に書き換える方法がありますが、今は解説しません。)マイクロソフトは後継のブラウザとしてMicrosoft Edgeを開発していますが、IE11は未だにWindows 8.1やWindows 10に搭載されており使う人は使うでしょう。実際の製品でES2015を使いたい場合はこのことに注意しましょう。

そんなES2015ですが、このページが最初に作られた段階(2014年)はES2015の機能はブラウザへの実装が徐々に進んでいるという段階でした。ES2015の機能の中でも特に早く利用可能になったのが今回紹介するWeakMapWeakSetです。ちなみに、WeakMapについては比較的早い段階で利用可能になったということで、実はIE11にも搭載されています。

実はWeakではないMapやSetもあるのですが、また今度紹介します(十六章第十三回)。

WeakMapとは

WeakMapというのは、その名の通り弱いMapです。まずMapというのはなにかというと、keyに対して値を保存しておけるものです。keyというのは第十四章のIndexedDBの説明でもでてきましたが、要するに各データに名前(key)を付けて保存しておき、名前(key)を指定すると保存したデータが取り出せるというものです。ただし、今回の話はlocalStorageIndexedDBのようにブラウザに保存しておける類のものではありません。プログラム中でデータを保持しておくのに利用するための単なるデータ構造です。

既に知っているものの中で今回紹介するWeakMapに近いのは、ただのオブジェクトです。

例えば、次のオブジェクトを見てみましょう。


var dict={
  "foo":"bar",
  "baz":5,
  "quux":null
};

このオブジェクトは、文字列"foo"に対して"bar"を、文字列"baz"に対して5を、文字列"quux"に対してnullを関連付けるものであると考えることができます。もちろん。JavaScriptのオブジェクトはあとからプロパティを追加することができますから、あとからキーと値のペアを増やすことも可能です。

このように、プロパティをキーとすることで、オブジェクトに好きな値を保存しておくことができるのです。

しかし、この方法にはひとつ欠点があります。それは、キーに文字列しか使用できないという点です。これは、キーがそもそもプロパティの名前であることを考えれば当然ですね。(注:ES2015でシンボルが追加されたのでまた話は違ってくるのですが、今回は考えないことにします。)

そこで、この問題を解決するのがWeakMapなのです。WeakMapでは文字列などではなく、オブジェクトをキーとして使います。オブジェクトをキーとして使えるということは、WeakMapを使ってオブジェクトに対して別の値を対応付けることができるということです。

WeakMapの使用

それでは、実際にWeakMapを使ってみましょう。

WeakMapを使うには、WeakMapの新しいインスタンスを作ります。

作ったら、setメソッドで新しいキーと値を保存できます。第1引数にキー、第2引数に値です。また、getメソッドでキーに対応する値を得ることができます。


var wm = new WeakMap();

//キーにはオブジェクトを使用する
var key1={};

wm.set(key1, 100); //key1に100を関連付ける

console.log(wm.get(key1));  //100が表示される

注意すべきことは、上のサンプルを実行したあとに次を実行しても100は表示されないということです(対応する値がないことを示すundefinedがgetの返り値となります)。


console.log(wm.get({}));

これは、{}に対応する値を得るということです。

さっきのサンプルではkey1に対して100を関連付けていて、key1は {} のはずなのに、なぜ次に {} に関連付けられた値を取得しようとしても存在しないということになるのでしょうか。

この答えを述べるならば、key1に入っている {} と次にgetの引数にした {} は別々のオブジェクトだからです。これは一章第二回で説明したことが関わっています。

オブジェクトリテラル{}は「新しいオブジェクトを作って返す」というはたらきを持ちます。つまり、key1の中のオブジェクトと {} で返されたオブジェクトは、形は同じでも別々のオブジェクトなのです。WeakMapのキーとしてオブジェクトを用いる場合は、この意味で“同じ”オブジェクトをキーとする必要があります。上の例では、key1に入っているまさにそのオブジェクトでないと100を取り出すことはできません。なお、オブジェクトを=====で比較すると同じオブジェクトであるときのみtrueになります。実際にkey1 === {}などとするとfalseになりますね。

なお、WeakMapでkeyにできるのはオブジェクトのみです。プリミティブをkeyにしようとするとエラーになります。

さて、WeakMapには他にも3つメソッドがあります。1つ目はhasです。これは引数としてキーを渡すと、そのキーに対する値がWeakMapに存在するか(setメソッドで登録されているかどうか)を真偽値で返します。存在しているならばtrueです。

2つ目はdeleteです。これは名前の通りで、引数にキーを渡すとそのキーに対応する値をWeakMapから削除します。

最後はclearです。引数なしで、WeakMap内の全ての値を消去します。

また、実はgetメソッドには第2引数を指定することができます。これはデフォルト値であり、対応する値が存在しなかった場合は第2引数の値が返されます。これを使うと、簡単な例ではいちいちhasメソッドを使わなくてもWeakMap内に値が存在しなかった場合の処理が書けるでしょう。

WeakMapの意味

さて、WeakMapが今までに比べて革新的なところは、やはり任意のオブジェクトに対して別の値を関連付けられるということです。

今まで、オブジェクトに対して値を関連づけたいならば、例えば適当なプロパティに保存しておくという方法がありました。つまり次のような感じです。


function saveValueInObject(obj,value){
  obj._myvalue=value;
}
function getValueFromObject(obj){
  return obj._myvalue;
}

この例では、オブジェクトに対して_myvalueというプロパティを勝手に作り、そこに保存しています。プログラムの他の部分で_myvalueを使っていなければ正しく保存できていることになりますが、万一_myvalueというプロパティが意味を持つようなオブジェクトがあれば、それを勝手に書き換えることになり思わぬ動作をする原因となります。

また、for-in文やObject.keysを使っている場合も変な動作になる可能性が高いですね。これについてはObject.definePropertyなどでenumerable属性がfalseのプロパティを作れば回避できますが、Object.getOwnPropertyNamesを使えばやはり見つけることができるので問題ないとはいえません。

他にも、組み込みオブジェクト(もともと定義されているオブジェクト)に対して勝手にプロパティを追加するのも望ましくありません。例えば、配列をはじめとするJavaScriptの言語仕様に存在するオブジェクトや、ノードのようなDOMオブジェクトが組み込みオブジェクトに該当します。これらに追加情報を関連付けたいときもWeakMapは活躍します。もちろん、WeakMapはどんなオブジェクトでもkeyとして利用できるので、関数やRegExp(正規表現オブジェクト)もOKです。

このように、keyとなるオブジェクトを汚さずに情報を付加できるのがWeakMapの1つの利点です。もう1つの利点は、関連付けた値が外部から見えないということです。

あるオブジェクトに対して関連付けた値は、当然WeakMapの中に保存されていると考えられます。ということは、当のWeakMapを持っていないと関連付けた値が分からないということになります。オブジェクトにプロパティとして勝手に付加する方式では、外部からそれを書き換えられてしまう可能性もあるのです。WeakMapならば、それを持っている自分しか値を読んだり書き換えたりすることができず、安全です。

WeakMapの特徴

さて、実はただのMapもあるという話をしましたが、実はこのようにキーと値を対応付ける機能をもつものをMapというので、いままでの説明で特にWeakな部分がありませんでした。

それではいったい何が弱くてWeakMapなのかというと、実は参照が弱いのです。

より詳しく言うと、WeakMapからキーのオブジェクトへの参照が弱いのです。

これを解説するには、ガベージコレクションの話をしなければなりません。まず、オブジェクトは突き詰めるとメモリ上にあります。つまり、オブジェクトの情報を覚えておくにはメモリが必要だということです。

ところで、次のサンプルを考えてみましょう。


var a = {
  foo:3,
  bar:5
};

a={};

最初に変数aにオブジェクト(オブジェクトAとしましょう)を代入しており、直後に別のオブジェクト(オブジェクトB)を代入しています。

オブジェクトAは当然メモリ上に作られます。その後、オブジェクトBもメモリ上に作られます。

2回目の代入ではオブジェクトBがメモリに入ります。すると、メモリ上のオブジェクトAはメモリから消さなければなりません。なぜなら、オブジェクトAはもう使われていないからです。より具体的には、オブジェクトAへの参照が存在しなくなったからです。参照が存在しないというのは、ざっくり言うとオブジェクトAがどの変数にも入っていないということです。これはすなわち、JavaScriptプログラムからオブジェクトAにアクセスする方法がなくなったということです。

このような場合、もう二度と使われないオブジェクトAをいつまでもメモリ上に取っておくのはメモリの無駄なので、自動的にメモリから破棄されます。このように、もう使われなくなったオブジェクトをメモリ上から消去する働きをガベージコレクションといいます。

では、次のサンプルはどうでしょう。


var a = {
  foo:3,
  bar:5
};

var wm = new WeakMap();
wm.set(a,100);

a={};

オブジェクトAをWeakMapのキーとして100を保存したあと、aにオブジェクトBを代入しました。

普通に考えると、wmの中にオブジェクトAをキーとした値が入っているので、getメソッドにオブジェクトAが渡されれば対応する値を返さなければいけません。そのためには、WeakMapはオブジェクトAを覚えておく必要があります。つまり、WeakMapの内部にオブジェクトAへの参照がなければいけません。そうでないと、getメソッドに渡されたオブジェクトがオブジェクトAと一致するかどうか調べられないからです。

しかしこのサンプルでは、変数aからオブジェクトAへの参照が無くなったことで、オブジェクトAへの参照はWeakMapの内部にしかない状態になりました。この場合、さっきと同じ理屈で、プログラムからオブジェクトAを取得する方法は無くなりました。なので、WeakMapのgetメソッドにオブジェクトAが渡されることはもう二度とないでしょう。ということは、WeakMapがオブジェクトAを覚えていても無駄ということです。WeakMapは偉いので、このように覚えていても無駄なオブジェクトは忘れてくれます。具体的にいうと、この場合オブジェクトAはガベージコレクションの対象となります。もちろん、WeakMapの中にオブジェクトAをkeyとして保存されていたデータも捨てられます。

このように、WeakMap内部にまだオブジェクトへの参照が存在していても、他のところに全くない場合はガベージコレクションの対象になります。言い方を変えると、WeakMap内部からオブジェクトへの参照はガベージコレクションを妨げないのです。このような参照を弱い参照といいます。これがWeakMapという名前の由来であり、WeakMapの大きな特徴でもあります。WeakMapのこの特性がなければ、使われなくなったオブジェクトがずっとメモリに残り続ける可能性があるのです。

この特性が役に立つ場面は結構ありますが、そんなに深く考えなくてもWeakMapはオブジェクトに対して値を関連付けられる便利なものとして使えます。

ちなみに、Weakではない普通のMapは後々紹介しますが、これはWeakMapとは異なり参照は弱くありません。つまり、同じ状況になった場合でもMap内部にオブジェクトへの参照が残っている限りガベージコレクションの対象とはなりません。これは、Mapが「キーとなっているオブジェクトの一覧」を返すメソッドなどを持っているため、Map内部にしかオブジェクトへの参照が無くなってもそれを外部から取得する手段があるからです。

逆に言えば、参照を弱くするという目的のためにこれらのメソッドをMapから削ったのがWeakMapであるということです。

WeakSet

さて、次のWeakSetについてですが、そもそもSetとは集合のことです。実はWeakSetは、オブジェクトの集合を表すものです。

一般に集合は、ある元が与えられたときにそれが集合に含まれるかどうかを判定できなければいけません。WeakSetは、集合にオブジェクトを追加するメソッド(add)、集合からオブジェクトを除去するメソッド(delete)、あるオブジェクトが集合に含まれるかどうか判定するメソッド(has)を持ちます。他に、集合に含まれるオブジェクトを全て除去するclearメソッドも持ちます。具体例を以下で見てみましょう。


var key={};
var ws=new WeakSet();

console.log(ws.has(key)); //wsにkeyは含まれないのでfalse

ws.add(key);

console.log(ws.has(key)); //さっき追加したのでtrue

console.log(ws.has({}));  //false

つまり、WeakSetはオブジェクトに対して「集合に含まれる」という情報を関連付けるものであると考えられます。こう考えると、WeakSetはWeakMapの機能を削減したバージョンであると考えられます。WeakMapはオブジェクトに任意の情報を紐付けることができるのに対し、WeakSetは「集合に含まれる」ということを示すのみです。

また、WeakSetには、WeakMapと同じ理由により「含まれるオブジェクトの一覧」を得る方法はありません(ふつうのSetにはあります)。

WeakSetはWeakMapに比べると使い所がないかもしれませんが、機会があれば使ってみましょう。