Object と Array の次に覚えたい2つのコレクション
JavaScriptで書くコードのほとんどは、素のオブジェクトと配列で事足ります。とはいえ、この2つがあらゆる用途に向いているわけではありません。そこで登場するのが Map と Set という組み込みのコレクションです。これらは「文字列以外をキーにしたルックアップ」と「重複排除したメンバーシップ判定」という、2つの具体的なニーズを埋めてくれます。
どちらもES2015から使える機能です。反復可能(iterable)で、.size プロパティを持ち、スプレッド構文とも相性がいい。イメージとしてはこんな感じです。
Map— オブジェクトに似ているが、キーに何でも使えて、挿入順も保持される。Set— 配列に似ているが、値は一意で、存在チェックが高速。
Map の作り方と使い方
Map はキーと値のペアを保持するコレクションです。new Map() でインスタンスを作り、.set()、.get()、.has()、.delete() で操作します。
コンストラクタに [key, value] のペアを並べた配列を渡して、初期値をまとめてセットすることもできます。
そのふたつの要素を持つ配列という形はMap絡みのあちこちに登場します。反復処理でエントリを表すときの基本形だからです。
MapとObject、どう使い分ける?
普通のオブジェクトでも同じようなことはできます。実際ほとんどの場面では事足ります。ただ、Mapには Object では引っかかりがちな部分をうまく解決してくれる場面がいくつかあります。
オブジェクトは Object.prototype を継承しているため、toString・constructor・hasOwnProperty といったキーが最初からすべてのオブジェクトに存在します。一方の Map にはそういった余計な荷物がなく、自分でセットしたキーだけが存在します。
その他に押さえておきたい違いは次のとおりです。
- キーの型を選ばない。 Map はオブジェクト・関数・数値・真偽値をキーにできます。一方のオブジェクトは文字列以外のキーを黙って文字列に変換してしまうので、
obj[1]とobj["1"]は同じ場所を指してしまいます。 - 挿入順が保証される。 Map は追加した順にイテレートされます。オブジェクトもおおむね同じですが、数値に見える文字列キーは先頭にソートされてしまうという地味な落とし穴があります。
- size プロパティが組み込み。
map.sizeは O(1) で取得できます。オブジェクトで同じことをやるならObject.keys(obj).lengthとなり、配列を毎回作り直すことになります。 - 出し入れの多い用途に最適化されている。 JavaScript エンジンは Map を頻繁な追加・削除向けにチューニングしています。オブジェクトは逆に、形が変わらないレコードとして扱われる前提で最適化されています。
キーが決まった文字列のレコード({ name, email, age } のようなもの)を表現したいときはオブジェクトを使いましょう。キーが動的だったり、文字列以外だったり、エントリの追加・削除が頻繁に発生する場面では Map の出番です。
Map を for...of で反復処理する
Map はイテラブルなので、for...of をそのまま使えますし、各エントリの分割代入もごく自然に書けます。
キーだけ、あるいは値だけが欲しい場合は .keys() や .values() を呼び出します。.forEach() のほうが好みならそちらでもOKです。
Map をただのオブジェクトや配列に戻したいときは、スプレッド構文を使います。
Set の作り方と使い方
Set は重複のない値だけを保持するコレクションです。すでに入っている値を追加しても何も起こりません(無視されるだけ)。
ユニーク判定は基本的に === と同じ等価ルールで行われますが、ひとつだけクセがあります。通常は NaN === NaN が false になるのに、Set の中では NaN は自分自身と等しいものとして扱われるのです。
コンストラクタにイテラブルを渡せば、その要素で Set を初期化できます。配列の重複排除によく使われるテクニックは、この仕組みを利用したものです。
1 行書くだけで、プリミティブ型ならなんでも重複排除できます。ただしオブジェクトの配列には効きません。同じフィールドを持つ別々のオブジェクトは「別の値」として扱われるためです。とはいえ、文字列・数値・真偽値に対してはこれが定番のやり方です。
Set と配列の使い分け
配列も Set も値のコレクションを保持できますが、どう使い分ければよいのでしょうか。
次のような場面では Set が向いています。
- 値の一意性を保証したいとき。ランタイム側で重複を弾いてくれます。
- 存在チェックを何度も行うとき。
set.has(x)は O(1) ですが、array.includes(x)は O(n) です。ループの中で使うと、この差はあっという間に効いてきます。 - 挿入順さえ保てればいいとき。Set は挿入順にイテレートできますが、インデックスでのアクセスはできません。
一方、次のような場面では配列のままにしておきましょう。
- 位置でアクセスしたいとき。
arr[0]や slice、sort を使う場合です。 - 重複に意味があるとき。たとえばショッピングカートに同じ商品が 2 つ入るケース。
.map、.filter、.reduceといった配列メソッドをがっつり使うとき。Set にはこれらがないので、いったん配列に展開する必要があります。
パフォーマンス差がわかりやすい例を見てみましょう。
もし banned が配列だったら、filter のコールバックが毎回リスト全体を舐めることになります。Set なら、検索は定数時間で済みます。
Set の反復処理
Map のときと同じ感覚で、for...of がそのまま使えます。スプレッド構文で配列に変換するのも簡単です:
Set にも Map との対称性のために .keys()、.values()、.entries() が用意されています。ただし Set ではキーと値が同じものなので、実際には直接イテレートして済ませることがほとんどです。
実践例:ページごとのユニーク訪問者数をカウントする
両方を組み合わせた例として、ページのパスをキー、訪問者IDの Set を値に持つ Map を作ってみます。
Map がパスごとのバケットへの振り分けを担当し、Set が各バケット内での重複排除を担当する、という役割分担になっています。これを普通のオブジェクトと配列だけでやろうとしてもできなくはないですが、indexOf での存在チェックや hasOwnProperty によるガードをあちこちに書く羽目になります。
WeakMap と WeakSet について少しだけ
限定的なユースケース向けに、WeakMap と WeakSet という関連コレクションも用意されています。これらは参照を弱く保持するのが特徴で、キー(WeakMap の場合)や値(WeakSet の場合)が他から参照されなくなると、そのエントリは自動的にガベージコレクションの対象になります。
キーに使えるのはオブジェクトだけで、イテレーションもできず、.size もありません。これは意図的な設計です。もし反復処理できてしまうと、ガベージコレクタの挙動が観測可能になってしまうからです。自分が所有していないオブジェクトに対してメタデータをキャッシュしたいとき役立ちますが、日常のコードで出番は多くありません。
次は JSON
Map と Set はメモリ上では便利ですが、どちらも JSON.stringify を通すとそのままの形では残りません。Map は {} に、Set も {} になってしまいます。次のページでは JSON を取り上げ、データのシリアライズとパースの方法、そしてこのページで扱ったコレクションをネットワークやファイルをまたいでやり取りするときのパターンを見ていきます。
よくある質問
MapとObjectの違いはどこにありますか?
大きな違いはキーに使える型です。Mapはオブジェクトでも関数でも数値でも、どんな値でもキーにできますが、プレーンなオブジェクトはキーが文字列(またはシンボル)に変換されてしまいます。さらにMapは.sizeで件数が取れる、挿入順で反復される、プロトタイプからキーを引き継がないのでtoStringやconstructorとぶつかる心配がない、といった利点もあります。キーが文字列以外だったり、エントリの追加・削除が頻繁な場面ではMapの出番です。
SetはJavaScriptでどんな場面に使いますか?
Setは重複を自動で弾いてくれる、ユニークな値だけを持つコレクションです。配列の重複排除なら[...new Set(arr)]が一番手早いイディオムですね。また、.has()がO(1)で動くので、ループの中で「この値は含まれているか?」を何度も判定するようなケースではarray.includes()より確実に速くなります。
Mapを反復処理するにはどうすればいいですか?
for...ofがそのまま使えます。for (const [key, value] of myMap)と書けば、各エントリを分割代入で取り出せます。必要に応じてmyMap.keys()、myMap.values()、myMap.entries()を回しても構いません。反復順は必ず挿入順になります。プレーンなオブジェクトだと数値っぽいキーの順序が保証されないので、ここもMapの強みです。