uhyohyo.net

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

十六章第一回 WeakMapとWeakSet

第十六章では、ECMAScript6の解説をします。これはECMAScript5(第十一章で解説)のさらに次のバージョンで、新しい機能、新しい文法などを備えています。

現在(2014年)、各ブラウザがECMAScript6の新機能を徐々に実装してきています。その中でも特に早く利用可能になったのが今回紹介するWeakMapWeakSetです。

実はWeakではないMapやSetもあるのですが、そちらはECMAScript6の新しい文法により深く関わっているので現段階では実装が進んでいないため、今回は紹介しません。

WeakMapとは

WeakMapというのは、その名の通り弱いMapです。まずMapというのはなにかというと、keyに対して値を保存しておけるものです。keyというのは第十四章のIndexedDBの説明でもでてきましたが、要するにあるものに対して別のものを関連付けておくことができるというものです。

このような機能をもつものは、今までのJavaScriptでも一応存在していました。ただのオブジェクトもその1つです。

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

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

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

このようにオブジェクトを用いればキーに対して値を保存しておくというのは可能なように見えますが、この方法にはひとつ問題があります。それは、キーに文字列しか使用できないということです。これは、キーがそもそもプロパティの名前であることを考えれば当然ですね。

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

WeakMapの使用

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

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

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

var wm = new WeakMap();

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

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

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

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

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

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

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

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

オブジェクトを変数に入れる場合、変数に入るのはオブジェクトの実体ではなくオブジェクトへの参照だと考えることができます。さらに、{}というのはオブジェクトリテラルである、「新しいオブジェクトを作って(その参照を)返す」というはたらきを持つと考えられます。

つまり、空のオブジェクトはvar key={};の行で1回、console.log(wm.get({}));でもう1回作られたことになり、別々に作られたのだから当然別のオブジェクトである(から参照も異なる)のです。このように、WeakMapは別々のオブジェクトを区別します。

o

また、keyとしてオブジェクト以外(プリミティブ)を使おうとした場合はエラーになります。

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

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

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

また、実はgetメソッドには第二引数を指定することができます。これは、対応する値が存在しなかった場合は第二引数の値を返すという仕様になっているためです。さきほど、対応する値がない場合は(第二引数がなければ)undefinedがgetの返り値になると述べたので、通常は返り値がundefinedかどうかで判定できそうなものですが、万一setメソッドで明示的にキーに対してundefinedを関連付ける場合などは、その結果がキーと関連付けられた値としてのundefinedなのか、対応する値がなかったという意味でのundefinedなのか区別できません。これを区別するためにはhasメソッドを使う必要があり面倒です。値が登録されてなかった場合に別の動作をさせたいという場合はどのみちhasメソッドが必要になりますが、値がなければかわりに0を用いたいとか、そういう「デフォルト値」を指定する用途ならばこのgetの第二引数は役立ちます。

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に代入されます。

すると、二回目の代入ではaに新しく作られたオブジェクトBへの参照が入り、もともと入っていたオブジェクト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への参照を保持している必要があります。そうでないと、getメソッドに渡されたオブジェクトがオブジェクトAと一致するかどうか調べられないからです。

しかしこのサンプルでは、その後やはりaにオブジェクトB(への参照)が代入されます。こうなると、オブジェクトAへの参照はWeakMapの内部にしかありません。この場合、さっきと同様に、WeakMapのgetメソッドにオブジェクトAへの参照が渡されることはないでしょう。すなわち、もはやオブジェクトAに対応する値を要求されることはないのですから、WeakMap内部にあるオブジェクトAへの参照は意味がないことになります。したがって、この場合もオブジェクトAはガベージコレクションの対象となります。

このように、WeakMap内部にまだオブジェクトへの参照が存在していても、他のところに全くない場合はガベージコレクションの対象になります。これを、(WeakMap内部の)「オブジェクトへの参照が弱い」と表現します。これがWeakMapという名前の由来であり、WeakMapの大きな特徴でもあります。

WeakMapのこの特性がなければ、使われなくなったオブジェクトが延々メモリに残り続ける可能性があるのです。

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

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

このような事情がある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に比べると使い所がないかもしれませんが、機会があれば使ってみましょう。