短気プログラマーのためのJavaScript(ES2022版)
この本をサポートしてください:購入する または 寄付する
(広告です。ブロックしないでください。)

33 マップ(Map



ES6以前は、JavaScriptには辞書のためのデータ構造がなく、文字列から任意の値への辞書としてオブジェクトを(不適切に)使用していました。ES6では、任意の値から任意の値への辞書であるマップが導入されました。

33.1 マップの使い方

Mapのインスタンスは、キーを値にマッピングします。単一のキーと値のマッピングは、_エントリ_と呼ばれます。

33.1.1 マップの作成

マップを作成するには、3つの一般的な方法があります。

まず、パラメータなしでコンストラクタを使用して、空のマップを作成できます。

const emptyMap = new Map();
assert.equal(emptyMap.size, 0);

次に、キーと値の「ペア」(2つの要素を持つ配列)に対する反復可能オブジェクト(例:配列)をコンストラクタに渡すことができます。

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'], // trailing comma is ignored
]);

3番目に、.set()メソッドはマップにエントリを追加し、チェーン可能です。

const map = new Map()
  .set(1, 'one')
  .set(2, 'two')
  .set(3, 'three');

33.1.2 マップのコピー

後述するように、マップはキーと値のペアに対する反復可能オブジェクトでもあります。そのため、コンストラクタを使用してマップのコピーを作成できます。そのコピーは_浅い_コピーです。キーと値は同じで、複製されません。

const original = new Map()
  .set(false, 'no')
  .set(true, 'yes');

const copy = new Map(original);
assert.deepEqual(original, copy);

33.1.3 単一エントリの操作

.set().get()は、(キーが与えられた場合の)値の書き込みと読み取りに使用します。

const map = new Map();

map.set('foo', 123);

assert.equal(map.get('foo'), 123);
// Unknown key:
assert.equal(map.get('bar'), undefined);
// Use the default value '' if an entry is missing:
assert.equal(map.get('bar') ?? '', '');

.has()は、マップに指定されたキーを持つエントリがあるかどうかを確認します。 .delete()はエントリを削除します。

const map = new Map([['foo', 123]]);

assert.equal(map.has('foo'), true);
assert.equal(map.delete('foo'), true)
assert.equal(map.has('foo'), false)

33.1.4 マップのサイズ決定とクリア

.sizeには、マップ内のエントリ数が含まれています。 .clear()は、マップのすべてのエントリを削除します。

const map = new Map()
  .set('foo', true)
  .set('bar', false)
;

assert.equal(map.size, 2)
map.clear();
assert.equal(map.size, 0)

33.1.5 マップのキーと値の取得

.keys()は、マップのキーに対する反復可能オブジェクトを返します。

const map = new Map()
  .set(false, 'no')
  .set(true, 'yes')
;

for (const key of map.keys()) {
  console.log(key);
}
// Output:
// false
// true

Array.from()を使用して、.keys()から返された反復可能オブジェクトを配列に変換します。

assert.deepEqual(
  Array.from(map.keys()),
  [false, true]);

.values().keys()と同様に機能しますが、キーの代わりに値に対して機能します。

33.1.6 マップのエントリの取得

.entries()は、マップのエントリに対する反復可能オブジェクトを返します。

const map = new Map()
  .set(false, 'no')
  .set(true, 'yes')
;

for (const entry of map.entries()) {
  console.log(entry);
}
// Output:
// [false, 'no']
// [true, 'yes']

Array.from()は、.entries()から返された反復可能オブジェクトを配列に変換します。

assert.deepEqual(
  Array.from(map.entries()),
  [[false, 'no'], [true, 'yes']]);

マップインスタンスは、エントリに対する反復可能オブジェクトでもあります。次のコードでは、分割代入を使用して、`map`のキーと値にアクセスしています。

for (const [key, value] of map) {
  console.log(key, value);
}
// Output:
// false, 'no'
// true, 'yes'

33.1.7 挿入順にリストされる:エントリ、キー、値

マップは、エントリが作成された順序を記録し、エントリ、キー、または値をリストする際にその順序を尊重します。

const map1 = new Map([
  ['a', 1],
  ['b', 2],
]);
assert.deepEqual(
  Array.from(map1.keys()), ['a', 'b']);

const map2 = new Map([
  ['b', 2],
  ['a', 1],
]);
assert.deepEqual(
  Array.from(map2.keys()), ['b', 'a']);

33.1.8 マップとオブジェクト間の変換

マップがキーとして文字列とシンボルのみを使用している限り、(Object.fromEntries()を介して)オブジェクトに変換できます。

const map = new Map([
  ['a', 1],
  ['b', 2],
]);
const obj = Object.fromEntries(map);
assert.deepEqual(
  obj, {a: 1, b: 2});

また、(Object.entries()を介して)文字列またはシンボルキーを持つオブジェクトをマップに変換することもできます。

const obj = {
  a: 1,
  b: 2,
};
const map = new Map(Object.entries(obj));
assert.deepEqual(
  map, new Map([['a', 1], ['b', 2]]));

33.2 例:文字のカウント

`countChars()`は、文字を出現回数にマッピングするマップを返します。

function countChars(chars) {
  const charCounts = new Map();
  for (let ch of chars) {
    ch = ch.toLowerCase();
    const prevCount = charCounts.get(ch) ?? 0;
    charCounts.set(ch, prevCount+1);
  }
  return charCounts;
}

const result = countChars('AaBccc');
assert.deepEqual(
  Array.from(result),
  [
    ['a', 2],
    ['b', 1],
    ['c', 3],
  ]
);

33.3 マップのキーに関する詳細(上級)

任意の値がキーになることができ、オブジェクトでも可能です。

const map = new Map();

const KEY1 = {};
const KEY2 = {};

map.set(KEY1, 'hello');
map.set(KEY2, 'world');

assert.equal(map.get(KEY1), 'hello');
assert.equal(map.get(KEY2), 'world');

33.3.1 どのキーが等しいとみなされるか?

ほとんどのマップ操作では、値がキーのいずれかと等しいかどうかを確認する必要があります。内部操作SameValueZeroを介して行います。これは`===`のように機能しますが、`NaN`は自身と等しいとみなします。

結果として、他の値と同様に、マップで`NaN`をキーとして使用できます。

> const map = new Map();

> map.set(NaN, 123);
> map.get(NaN)
123

異なるオブジェクトは、常に異なるとみなされます。これは変更できないものです(ただし、キーの等価性の設定は、TC39の長期ロードマップにあります)。

> new Map().set({}, 1).set({}, 2).size
2

33.4 マップに存在しない操作

33.4.1 マップのマッピングとフィルタリング

配列を`.map()`および`.filter()`できますが、マップにはそのような操作はありません。解決策は次のとおりです。

  1. マップを[key, value]ペアの配列に変換します。
  2. 配列をマッピングまたはフィルタリングします。
  3. 結果をマップに戻します。

次のマップを使用して、その仕組みを説明します。

const originalMap = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');

`originalMap`のマッピング

const mappedMap = new Map( // step 3
  Array.from(originalMap) // step 1
  .map(([k, v]) => [k * 2, '_' + v]) // step 2
);
assert.deepEqual(
  Array.from(mappedMap),
  [[2,'_a'], [4,'_b'], [6,'_c']]);

`originalMap`のフィルタリング

const filteredMap = new Map( // step 3
  Array.from(originalMap) // step 1
  .filter(([k, v]) => k < 3) // step 2
);
assert.deepEqual(Array.from(filteredMap),
  [[1,'a'], [2,'b']]);

`Array.from()`は、任意の反復可能オブジェクトを配列に変換します。

33.4.2 マップの結合

マップを結合するためのメソッドがないため、前のセクションと同様の回避策を使用する必要があります。

次の2つのマップを結合してみましょう。

const map1 = new Map()
  .set(1, '1a')
  .set(2, '1b')
  .set(3, '1c')
;

const map2 = new Map()
  .set(2, '2b')
  .set(3, '2c')
  .set(4, '2d')
;

`map1`と`map2`を結合するために、新しい配列を作成し、`map1`と`map2`のエントリ(キーと値のペア)を(反復を介して)スプレッド(`...`)します。次に、配列をマップに戻します。これらはすべて行Aで行われます。

const combinedMap = new Map([...map1, ...map2]); // (A)
assert.deepEqual(
  Array.from(combinedMap), // convert to Array for comparison
  [ [ 1, '1a' ],
    [ 2, '2b' ],
    [ 3, '2c' ],
    [ 4, '2d' ] ]
);

  演習:2つのマップの結合

exercises/maps/combine_maps_test.mjs

33.5 クイックリファレンス:`Map<K,V>`

注:簡潔にするために、すべてのキーが同じ型`K`を持ち、すべての値が同じ型`V`を持つと仮定しています。

33.5.1 コンストラクタ

33.5.2 `Map<K,V>.prototype`:単一エントリの処理

33.5.3 `Map<K,V>.prototype`:すべてのエントリの処理

33.5.4 `Map<K,V>.prototype`:反復処理とループ

反復処理とループはどちらも、エントリがマップに追加された順序で行われます。

33.5.5 このセクションの情報源

33.6 FAQ:マップ

33.6.1 いつMapを使い、いつオブジェクトを使うべきか?

文字列でもシンボルでもないキーを持つ辞書のようなデータ構造が必要な場合は、選択肢がありません。マップを使用する必要があります。

ただし、キーが文字列またはシンボルの場合は、オブジェクトを使用するかどうかを決定する必要があります。大まかな一般的なガイドラインは次のとおりです。

33.6.2 いつMapのキーとしてオブジェクトを使うか?

通常、マップキーは値によって比較されるようにします(2つのキーは、同じ内容であれば等しいとみなされます)。これにはオブジェクトは含まれません。ただし、オブジェクトをキーとして使用するユースケースが1つあります。オブジェクトに外部からデータを添付する場合です。ただし、そのユースケースはWeakMapの方が適しています。WeakMapでは、エントリによってキーがガベージコレクトされるのを防ぐことはありません(詳細は、次の章を参照してください)。

33.6.3 なぜマップはエントリの挿入順序を保持するのか?

原則として、マップは順序付けされていません。エントリを順序付ける主な理由は、エントリ、キー、または値をリストする操作を決定論的にするためです。これは、たとえばテストに役立ちます。

33.6.4 なぜマップは`.size`を持ち、配列は`.length`を持つのか?

JavaScriptでは、インデックス付きシーケンス(配列や文字列など)は`.length`を持ち、インデックスなしコレクション(マップやセットなど)は`.size`を持ちます。

  クイズ

クイズアプリを参照してください。