uhyohyo.net

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

十六章第十三回 MapとSet

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

今回はMapSetの解説です。しばらくES2015の新しい文法の話をしてきましたが、それもほとんど終わりました。ここからは新しい組み込みオブジェクトを中心として話をしていきます。

この章の第一回でWeakMapとWeakSetの話をしたのを覚えているでしょうか。

今回はそのWeakでない版、すなわち普通のMapSetを紹介します。

基本的な使い方はWeakMap・WeakSetと同じです。どちらもnewで作り、Mapはsetメソッドで値を追加し、hasメソッドで値があるかどうか確認できます。また、getメソッドでキーに対応する値を取り出せます。Setはaddで要素を追加し、hasで値があるか確認します。どちらもdeleteメソッドやclearメソッドを持ちます。

ただし、WeakMap・WeakSetはキーとしてオブジェクトしか使えなかったのに対して、Map・Setではキーにたいして任意の値を使うことができます。端的に言えば、プリミティブも使えるということです。


var m = new Map();

m.set(2, 'foo');
m.set(null, 'bar');

console.log(m.get(2), m.get(null)); // foo bar

オブジェクトをキーにする場合は、WeakMapやWeakSetと同様に、同じオブジェクトでないと値を取り出すことはできません。プリミティブの場合も、setと同じ値をgetに渡すと値を取り出すことができます。ここで「同じ」というのは、===演算子で比較して同じということです。ただし、ひとつ例外があります。それはNaNです。NaNは===でNaNどうしを比較してもfalseになるという特異な値でした。しかし、MapやSetのキーとして使う場合はNaNどうしは同じ値と見なされます。つまり、NaNをキーとしてsetしたものはNaNをキーとしてgetで取り出すことができます。

そして、WeakMapやWeakSetとMapやSetで異なる点は、後者らは今中に何が入っているかという情報が使えるという点です。前者はキーへの参照がWeakでないといけなかったので、いまどんな値が中に入っているのか(すなわちどんなキーが今使われているのか)という情報を得ることができませんでした。MapやSetではこのような情報を得ることができます。具体的には、そのためのメソッドがWeakMapやWeakSetに比べて増えています。

では、それらのメソッドを紹介していきます。

Map

まずはMapですが、先にコンストラクタの機能を紹介しておきます。今まではまず空のmapを作ってsetで要素を追加していましたが、コンストラクタの引数により、最初から中に要素が入った状態でMapを作ることができます。それには、[キー, ]という2要素の配列を1つの組とし、この組の配列をコンストラクタに渡します。


var m = new Map([['alpha', 'α'], ['beta', 'Β'], ['gamma', 'γ']]);

console.log(m.get('alpha')); // 'α'
console.log(m.get('beta')); // 'Β'
console.log(m.get('gamma')); // 'γ'

ただし、ここでは配列といいましたが、iterableであればなんでもよいです。そのため、Array#entriesを使えばこのように配列をMapに変換できます。


var arr = ['John Smith', 'Taro Tanaka', 'Gonbe Nanashino'];
var m = new Map(arr.entries());

console.log(m.get(0)); // John Smith

これはサンプルの説明に便利なので使っていきます。

では、メソッドの説明に移ります。forEachメソッドは、Mapに入っている全ての要素に対して順にコールバック関数を呼び出すメソッドです。コールバック関数の引数はArray#forEachと同様に、第1引数が値、第2引数がキーとなります。さらに、第3引数には勝利されているMapオブジェクトそのものが入っています。


var m = new Map([[Boolean, 'Boolean'], [Number, 'Number'], [String, 'String']]);

m.forEach((value, key)=>{
  console.log(`${value}(3):`, key(3));
});

余談ですが、このようにサンプルでも最近紹介したテンプレート文字列アロー関数などをバシバシ使っていきます。もし分からなければ戻って復習しましょう。

また、上でArray#entriesが話題に出ましたが、Mapもkeys, values, entriesの3つのメソッドを持ちます。これはそれぞれキー、値、そして[キー, 値]ペアのイテレータを返します。イテレータなので、for-of文で回したりArray.fromで配列にしたりできます。例えば、上のforEachの例は次のようにできるでしょう。


var m = new Map([[Boolean, 'Boolean'], [Number, 'Number'], [String, 'String']]);

for (const [key, value] of m.entries()){
  console.log(`${value}(3):`, key(3));
}

forEachを使うよりこちらのほうが今どきですね。for文なのでcontinueやbreakも使えます。

もっと言えば、Mapのインスタンスは@@iteratorメソッドも持ちます。これはシンボルの回でやったwell-knownシンボルのひとつ、Symbol.iteratorのことでしたね。@@iteratorメソッドを持つオブジェクトはiterableです。なお、@@iteratorメソッドが返すイテレータはentriesメソッドと同様のものです。

ということは、Mapオブジェクト自体がiterable上の例のようにentries()を使ってfor文を回すときはentries()を呼び出す必要すらないということです。


var m = new Map([[Boolean, 'Boolean'], [Number, 'Number'], [String, 'String']]);

for (const [key, value] of m){
  console.log(`${value}(3):`, key(3));
}

なお、イテレートされる順番はMapに追加された順番となります。

これらから分かることは、Mapでは[キー, 値]というペアが基本単位として使われており、コンストラクタやイテレータにおいて現れてくるということです。

そして、なんとMapのメソッドはこれで終わりです。他に、プロパティとしてsizeがあります。これは配列でいうlengthみたいなもので、そのMapに入っているkey-valueペアの数を整数で返します。


var arr = ['John Smith', 'Taro Tanaka', 'Gonbe Nanashino'];
var m = new Map(arr.entries());

console.log(m.size); // 3

Set

ではSetの話に移ります。先ほど、[キー, 値]がMapの基本単位だと述べましたが、Setの場合は値が入っているかどうかという情報しかないので、値そのものが基本単位となります。

よって、コンストラクタでSetの要素を指定するには例えば次のようにすればよいことになります。この例では、sには"foo""bar"3という3つの値が入ったSetが入ります。


var s = new Set(['foo', 'bar', 3]);

console.log(s); // Set {"foo", "bar", 3}
console.log(s.has('foo')); // true

SetのメソッドはMapのものと似ています。add, clear, delete, hasはWeakSetと同じです。また、valuesメソッドが存在し、これはSetに入っている値をひとつずつ取り出すイテレータを返します。これを用いるとSetの全ての要素を順に処理することができるでしょう。


var s = new Set(['foo', 'bar', 3]);

for (const value of s.values()){
  console.log(value);
}

ほかの2つのメソッド、すなわちentrieskeysも実はあります。特にentriesは、Setの場合は少し変な働きをします。entriesを呼び出すとやはりイテレータを返しますが、Setの各要素に対して[値, 値]という形の配列を返します。次の例で確かめてみましょう。


var s = new Set(['foo', 'bar', 3]);

for (const value of s.entries()){
  console.log(value);
}

この例を実行すると、次のようなログが出るはずです。


['foo', 'foo']
['bar', 'bar']
[3, 3]

どんな使いどころがあるのかはとても微妙ですが、返されるオブジェクトがMapと同じ形をしているのがポイントです。MapとSetを同様に扱いたい場合に役に立つかもしれません。

Mapのvaluesがこのペアの右側の値をイテレートするイテレータを返したのに対して、keysは左側(すなわちキー)のイテレータでした。Setのkeysも同様です。といっても、Setの場合は左右は常に同じなので結局keysはvaluesと同じ挙動をすることになります。

もちろんSetにも@@iteratorメソッドがあり、これはvaluesと同じ挙動です。よって、次のようにしてSetの要素を処理できます。


var s = new Set(['foo', 'bar', 3]);

for (const value of s){
  console.log(value);
}

あと、例によってforEachメソッドとsizeプロパティがあります。

以上でMapとSetの紹介は終わりです。WeakMapやWeakSetと比べると、entriesに代表される、中身の一覧を得られるようなメソッドが増えています。もちろん、これによりWeakMapやWeakSetの特徴であった「参照が弱い」という性質は失われています。