ES2015で導入されたMapオブジェクトを使ってみる

f:id:tsubasa123:20170119171032j:plain

こんにちは、つばさ(@tsubasa123)です。

存在すら知らなかった新しいデータ構造、Mapについて簡単に使い方を調べてみました。ES2015、色々変わりすぎだよ。がんばって時代の流れに追いつきます。

何に使うの?

キー・バリューの関係を保持することができるもので、今までのオブジェクトで行ってきたようなことを、セッターとゲッターメソッド経由で行うことができるようになります。Mapオブジェクトを作成する時はnew演算子を利用します。

const m = new Map();
m.set('name', 'hoge');
console.log(m.get('name')); // hoge

こんな感じで使えます。簡単ですね。ですが、これでは普通にオブジェクトで代用することも可能です。

const m = {};
m.name = 'hoge';
console.log(m.get('name')); // hoge

はい、同じ結果です。わざわざnew演算子を利用してまでMapオブジェクトを利用するメリットは無いように思えます。私は最初「意味ないじゃん」と思いました。

いつ使うの?

developer.mozilla.org

MDNのリファレンスを見てみると、

これは Map をいつも使えばいいということではありません。オブジェクトはまだ多くの場面で使えます。Map インスタンスはコレクションとして使う場合のみに役に立ちます。以前オブジェクトをこのように使っていたコードに Map を使うことを考えてみるべきです。

とのこと。つまり、ただ単純にキー・バリューをペアで持たせるのではなく、キー・バリューのようなデータ構造を複数扱うコレクションとして利用していたオブジェクトのかわりにMapオブジェクトを使うといい感じになるようです。

オブジェクトの代用というよりは、配列の代用に近いイメージ。違うかな?純粋な配列だとキーは添え字にするしかありませんが、オブジェクトであれば任意の値をキーにして、複数の要素を格納することができます。ただし、配列にはforEachのような格納要素を順番に処理するための機構が用意されていますが、オブジェクトにはそのようなものはありません。

Mapオブジェクトはこの2つの違いのいいとこどりしたようなもの、といったところでしょうか。なんとなくそんな印象を受けました。複雑なデータ構造を作るために利用したオブジェクトをインデックスではなく、任意のキーを利用してコレクションするための構造、なのかな。

色々調べてみたり、コードを書いてみたりしたところ、私が「Mapって結構便利かも」と思ったポイントが3つほどありました。ちょっとブログの記事にするためにメリットをこじつけた感があったりしますが、まとめてみます。

要素数を簡単に取得できる

真っ先に思いついたのがこれですね。プロパティアクセスだけでコレクションに格納されている要素の数を取得できるようになります。

const arr = [1, 2, 3, 4, 5];
console.log(arr.length); // 5
const obj = {a: 1, b: 2, c: 3, d: 4, e: 5};
console.log(obj.length); // undefined
const map = new Map([['a', 1], ['b', 2], ['c', 3], ['d', 4], ['e', 5]]);
console.log(map.size); // 5

配列の場合はlengthプロパティで要素数を取得できましたが、Mapオブジェクトの場合はsizeプロパティが同じような役割をしてくれます。間違えないようにしましょう。ちなみに、オブジェクトをコレクションに利用している場合でも、

const obj = {a: 1, b: 2, c: 3, d: 4, e: 5};
console.log(Object.keys(obj).length); // 5

このようにワンクッション挟めばlengthプロパティが使えるようになります。覚えておくと幸せになれる日がくるかもしれません。

キーにオブジェクトを利用できる

インパクトが強かったのがこれ。今までのオブジェクトではキーにはプリミティブな値、というか、「文字列」もしくは「数値」を指定して使うことしかできなかった(ES2015からSymbolも使えるけど省略)ですが、Mapオブジェクトの場合はキーにオブジェクトを指定することも可能になります。

const map = new Map();
const o1 = {name: 'hoge'};
const o2 = {name: 'moge'};
const o3 = {name: 'fuga'};
map.set(o1, 'hogehoge');
map.set(o2, 'mogemoge');
map.set(o3, 'fugafuga');
console.log(map.get(o1)); // hogehoge

こんな感じで利用可能です。すげー、これは便利かも。

具体的には、オブジェクトに対して一時的に追加データを持たせたいけど、そのオブジェクトのプロパティとして直接データをセットしたくない、といった時に使えそうな気がします。任意のDOM要素が何回クリックされたかを、DOM要素をキーに、クリック回数をバリューにしてMapに格納、とかすると捗りそうですね。従来ならDOM要素のクラスとかdata属性とかにユニークな文字列を持たせる必要がありましたが、それが不要になります。

const map = new Map();
$('#hoge').on('click', function() {
let c = map.get(this) || 0;
map.set(this, ++c);
console.log(map.get(this)); //  1, 2, 3...
});

こんな感じ。これ便利!

反復処理が可能

最後はループ処理の話。Array.prototype.forEachと同じように、MapオブジェクトもforEachメソッドを利用して反復処理を行うことができます。

const map = new Map();
map.set('hoge', 'hogehoge');
map.set('moge', 'mogemoge');
map.set('fuga', 'fugafuga');
map.forEach(function(v, k) {
console.log(`${k} -> ${v}`);
// hoge -> hogehoge
// moge -> mogemoge
// fuga -> fugafuga
});

はい、便利。forEachだけでなく、for…ofでもイテレーションが可能となっています。

const map = new Map();
map.set('hoge', 'hogehoge');
map.set('moge', 'mogemoge');
map.set('fuga', 'fugafuga');
for (let [k, v] of map) {
console.log(`${k} -> ${v}`);
// hoge -> hogehoge
// moge -> mogemoge
// fuga -> fugafuga
}

はい、これも便利。キー・バリューの代入部分は分割代入(デストラクチャリング)が用いられています。これはなんかのAltJSで使ったことがあったのでなんとか理解できましたが結構初見殺しなシンタックスな気がします。

さいごに

ES2015のMapオブジェクトの特徴をまとめてみました。なんか便利そう、ってことは把握。「キーにオブジェクトが使える」はかなり実用できそうな仕様ですね。例でも書きましたがDOM操作時に捗ると思います。本当はSetオブジェクトまで書きたかったですが、力尽きたので今日はここまで。

ではでは、最後までお付き合いいただきありがとうございました。