kyohei's blog

profile picture
Written by Kyohei Tsukuda who lives and works in Tokyo 🇯🇵 , building useful things 🔧.
email / facebook / X / GitHub

JavaScript Map vs Object(実装編)

March 23, 2022 - JavaScript Map Object

JavaScript では単純な連想配列(ハッシュマップ)を実装する場合、Map または Object を利用することが多いと思う、使い方がよく似ているためどちらを選択していいか迷うときがある、今回はその違い、パフォーマンス、使い所をまとめてみた。

そもそも...

JavaScript は元来(ES5 までは)、プリミティブな型以外の”データの集合”を表す場合、すべて Object となっており、Array や Function も実は Object である。

ES6 において、class や Set などデータの集合を表すための表現がいくつか追加され、その中の一つに Key-Value のデータを扱うための新しいアプローチとして Map も追加された。

とはいえ、JSON.parse のように、純粋な Object 型を使うケースはまだまだ存在し、どのようなタイミングで Map を利用するかが不明瞭だったので、それを少し調べてみた。

Map VS Object

Key

Map の一番の特徴は Key に String 以外の値を設定できるところかと思う。

const m = new Map();
// こういったケースが可能
m.set(true, false);
m.set([0], '1');
m.set({}, '2');
m.set(() => {}, '3');

また、Object 型のインスタンスは Object の prototype を参照することになるので、valueOf や toString などのキーをデフォルトで持つことになる。また、最近は見なくなったけど、Object.prototype を拡張している場合そのプロパティも参照できてしまうことになる。(そのため、hasOwnProperty を挟むとか昔はよくあった)

const x = {};
x['toString'] => Function

Object.prototype.y = 'yyy';
x.yyy => 'yyy'

for(const key in x){ console.log(key) } // yが取得できる

Map ではこのようなことが起きない。

デフォルトの値

Map ではコンストラクタの引数に key-valeu 配列の配列を渡すことでデフォルトの値を定義することができる。

const m = new Map([
  ['key', 1],
  ['key2', 2]
]);
m.get('key'); // 1

Object では宣言時のオブジェクトリテラル {} 内に値を定義する

const o = { key: 1, key2: 2 };
o.key; // 1

値の登録

Map では set メソッドを利用して値を登録する。key が重複した場合は当然値が上書きされる。

const m = new Map();
m.set('key', 1);
m.set('key', 2); // keyの値を上書き

Object では . で key を直接指定、またはハイフンを含むキーや全角文字の場合クォートを付けた文字列リテラルまたは文字列の変数を利用して、設定する方法がある。

const o = {};
o.key = 1;
o['key-2'] = 2;

値の取得

Map における値の取得は get メソッドを利用する。key が存在しない場合は undefinde が返る。

const m = new Map();
m.set('key', 1);
m.get('key'); // 1
m.get('key123'); // undefined

Object は設定時と同様で . で key を直接指定、または [] 内に文字列を入れて呼び出す方法がある。キーが存在しない場合は undefined が返る。

const o = {};
o.key = 1;
o.key; // 1
o.key123; // undefined

値の削除

Map では delete メソッドを利用して指定のキー及び値を削除することができる。削除が成功すると、 true を返し、key が存在しない場合は、 false を返す。

const m = new Map();
m.set('key', 1);
m.delete('key'); // true
m.delete('key123'); // false;

また、map 内のすべての値を削除する clear メソッドも用意されている。

m.clear();

Object では、 delete 演算子を用いてキー及び値を削除する。キーが自分自身の non-configurable のプロパティであった場合、 strict モードでなければ false を返し、それ以外は true を返す。つまりキーが存在しない場合でも true を返す。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_delete

const o = { key: 1 };
delete o.key; // true
delete o.y; // true

キーの存在確認

Map でキーの存在を確認する場合は has メソッドを利用します。キーが存在する場合は true、存在しない場合は false を返す。

const m = new Map();
m.set('key', 1);
m.has('key'); // true
m.has('key123'); // false

Object では hasOwnProperty メソッドで自身のオブジェクトにあるキーかをチェックすることができる。

const o = { key: 1 };
o.hasOwnProperty('key'); // true
// もしくは
Object.prototype.hasOwnProperty(o, 'key'); // true

要素数の確認 / 空チェック

Map では size メソッドが用意されており、簡単に要素数を取得できる。値が 0 であるかを判定する事によって empty かのチェックもできる。

const m = new Map([['key', 1]]);
m.size; // 1

Object では Object.entriesObject.keysObject.values にて配列に変換し、その要素数を取得するというのが一般的なやり方である。一度配列に変換するので、コストがかかるという印象。

余談だが、 Object.keys よりも Object.valuesの方が Chrome,Firefox ではパフォーマンスが良い

const o = { key: 1 };
Object.values(o).length; // 1

走査

Map, Object ともに走査の方法はいくつか存在する。

Map はイテレータブルなオブジェクトなので、 for .. of が利用できる。この場合、[ key, value ]のタプルような形で値を取得される。

const m = new Map([
  ['key1', 1],
  ['key2', 2]
]);

for (const p of m) {
  console.log('key', p[0], 'value', p[1]);
}
// こちらと同等の処理である
for (const p of map.entiers()) {
  console.log('key', p[0], 'value', p[1]);
}

keys , values メソッドでキーのみ、値のみのイテレータブルなオブジェクトを取得することができる。

map オブジェクトに実装されている forEach を利用しても同様に走査ができる。(Array の forEach とは引数が異なるので注意)

m.forEach((v, k) => console.log(k, v));

イテレータブルなオブジェクトは Array.from または [...x] を利用して配列に変換できるため、従来の配列のメソッドを利用したい場合はこちらのパターンを利用する。

[...m].forEach(([k, v]) => console.log(k, v));
// または [...m.entiers()],[...m.keys()],[...m.values()]

Object の場合は for..in を利用して走査ができる。

ただし、この場合、前述の通り Object.prototype に拡張されているプロパティまで参照されてしまう。なので、hasOwnProperty 等でチェックする必要がある。

const o = {key1: 1, key2: 2};
Object.prototype.x = 3
for (const p in o) {
  console.log('key', p, 'value', o[p]);
  // key key1 value 1
  // key key2 value 2
  // key x value 3  <- !!
}

for (const p in o) {
  if(o.hasOwnProperty(p){
    console.log('key', p, 'value', o[p]);
  }
  // key key1 value 1
  // key key2 value 2
 }

結論から言うと、for..in を使うケースは今後ほぼないと思います。 代わりに Object.entries() Object.key() Object.values() のいずれかを使い、配列に変換し、Array のメソッドもしくは for..of を利用して走査するのが一般的です。

Object.entries(o).forEach(([k, v]) => console.log(k, v));

// または

for (const p of Object.entries(o)) {
  console.log(p[0], p[1]);
}

Map のオブジェクト、配列への変換

ES2019 から加わった Object.fromEntries を使うことにより、オブジェクトへの変換が可能、

また、 Array.from で、配列に変換できます。

const m = new Map([
  [1, 2],
  [3, true]
]);

Object.fromEntries(m);
// {1: 2, 3: true}

Array.from(m);
// [[1, 2], [3, true]]

JSON へのシリアライズ

Map の場合、キーに文字列以外を指定できるため、原則的には直接変換できないものと思われる。

上記の Object.fromEntries を利用し、オブジェクトに変換し、それをシリアライズすることは可能。ただし、キーに文字列以外が指定されていると意図しない結果になるので注意。

const m = new Map([[1, 1]]);
const o = { obj: '123' };
m.set(o, 100);
JSON.stringify(Object.fromEntries(m));
// '{"1":1,"[object Object]":100}'  🤔

また、配列に変換しそれをシリアライズすることが可能。こちらもキーに関数やクラスのインスタンスなど設定すると意図しないものとなるので注意。

const m = new Map([[1, 1]]);
const o = { obj: '123' };
m.set(o, 100);

JSON.stringify([...m]); // もしくは Array.from(m.entries())など
// => [[1,1],[{"obj":"123"},100]]

// キーに関数やクラスのインスタンスなど設定すると意図しないものとなるので注意
const m2 = new Map();
const f = (x) => x + x;
class A {
  get() {
    return 'get';
  }
}
const aa = new A();
m2.set(f, 'func');
m2.set(aa, aa);

JSON.stringify([...m2]);
// => [[null, 'func'],[{}, {}]] 🤔

まとめ

MapObject
constructorconst m = new Map()const o = {}
値の登録m.set(key, value)o.key = value
o[key] = value
値の取得m.get(key)o.key
o[key]
値の削除m.delete(key)
m.clear()delete o.key
delete o[key]
プロパティの存在確認m.has(key)o.hasOwnProperty(key)

| プロパティ数の確認 空かどうかのチェック | m.size m.size === 0 | Object.values(o).length Object.values(o).length === 0 | | 走査 | for(const p of m){...} m.forEach((v, k) => {...}); [...m].forEach((p) => {...}) | Object.entries(o).forEach((p) => {...}) for(const p of Object.entries(o)){...} | | オブジェクト化、配列化 | Object.fromEntries(m) Array.from(m) | Object.entries(o) |

同等のことができとわかったので、次回はパフォーマンスを見てみたいと思います。

次回 ↓

JavaScript Map vs Object (パフォーマンス編) | kyohei's blog