undefined
かどうかでチェックする構文をよく利用する。この場合のコーナーケースはキーはあるけど値が undefined
である場合と、そもそもキーがない場合とで見分けがつかないことである。
とはいえ、できれば正しく判定メソッドを利用すべきである。JavaScriptでは単純な連想配列(ハッシュマップ)を実装する場合、MapまたはObjectを利用することが多いと思う、使い方がよく似ているためどちらを選択していいか迷うときがある、今回はその違い、パフォーマンス、使い所をまとめてみた。
JavaScriptは元来(ES5までは)、プリミティブな型以外の”データの集合”を表す場合、すべてObjectとなっており、ArrayやFunctionも実はObjectである。
ES6において、classやSetなどデータの集合を表すための表現がいくつか追加され、その中の一つにKey-Valueのデータを扱うための新しいアプローチとしてMapも追加された。
とはいえ、JSON.parseのように、純粋なObject型を使うケースはまだまだ存在し、どのようなタイミングでMapを利用するかが不明瞭だったので、それを少し調べてみた。
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
undefined
かどうかでチェックする構文をよく利用する。この場合のコーナーケースはキーはあるけど値が undefined
である場合と、そもそもキーがない場合とで見分けがつかないことである。
とはいえ、できれば正しく判定メソッドを利用すべきである。Mapでは size
メソッドが用意されており、簡単に要素数を取得できる。値が0であるかを判定する事によってemptyかのチェックもできる。
const m = new Map([['key', 1]]);
m.size // 1
Objectでは Object.entries
、 Object.keys
、 Object.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]) }
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]]
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'],[{}, {}]] 🤔
Map | Object | |
---|---|---|
constructor | const 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) |
同等のことができとわかったので、次回はパフォーマンスを見てみたいと思います。
次回↓