【JavaScript】Mapって何？

# Mapとは
`Map`オブジェクトは**キーと値のペアを持つ**オブジェクトです。

データ構造はオブジェクトリテラルと似ていますが、`Map`は特にキーが変動する**連想配列**に使えます。

## 使い方

コンストラクタから作成できます。
```js
const map = new Map()
```

キーと値のペアを登録するには`set`を使います。
```js
map.set('text1', 'Hello World!')
map.set('text2', 'JavaScript')
```

キーから値を取得するには`get`を使います。

```js
map.get('text1') // 'Hello World!'
map.get('text2') // 'JavaScript'
```

# よく使うメソッド
`Map`オブジェクトからデータを取得したり操作するには、メソッドを使うのが一般的です。
ここではその中でもよく使うものを紹介します。

## [`get`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map/get), [`set`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map/set)
この2つは`Map`の最も基本的なメソッドで、値を登録したり取得したりできます。

- `get`: キー対応する値を取得する
- `set`: キーと値のペアを登録する

```js
const map = new Map()
map.set('key', 'value')
map.get('key') // 'value'
```

なお、`get`で指定されたキーが見つからなかった場合、`undefined`が返されます。

```js
const map = new Map()
map.get('not-found') // undefined
```

## [`has`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map/has)
`Map`の中に与えられた**キーが登録されているか**を調べます。
戻り値は登録されているかを示す`boolean`です。

```js
const map = new Map()
map.set('key', 'value')

map.has('key') // true
map.has('not-found') // false
```

`has`を使うと、キーが存在するかどうかで条件分岐ができます。
例えば、

- キーが存在する: その値を使って何か処理をする
- キーが存在しない: そのキーに何か値を登録する

といったことができます。

```js
if (map.has(key)) { // 条件分岐
  const value = map.get(key)
  // ...
} else {
  // ...
  const value = // 登録したい値
  map.set(value)
}
```

## [`forEach`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach)
値とキーのペアに対して、与えられた関数（`callbackFn`）を順番に実行します。

`callbackFn`は以下の引数で実行されます。

1. `value`: ペアの値
1. `key`: ペアのキー
1. `map`: `Map`オブジェクトそのもの

`value`が`key`より**先に来る**ので注意しでください。

```js
const map = new Map()
map.set('key1', 10)
map.set('key2', 20)

map.forEach((value, key) => {
  console.log(`${key}: ${value}`)
})

// key1: 10
// key2: 20
```

## 削除系
ペアの削除には2つのメソッドが使えます。

- [`delete`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map/delete): 与えられたキーのペアを削除
- [`clear`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map/clear): 全てのペアを削除


## コンストラクタ

コンストラクタに[反復可能オブジェクト](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Iteration_protocols)を入れることで、`Map`の初期値を設定できます。
要素は`[キー, 値]`形式のArray-likeオブジェクトである必要があります。

反復可能やArray-likeには、例えば`Array`があります。
配列を使うと、例えば以下のようにオブジェクトを作成できます。

```js
const map = new Map([
  ['key', 'value'],
  ['key2', 10],
])

map.get('key') // 'value'
map.get('key2') // 10
```

### Array-Like
ここでのArray-Likeとは、`[]`を使って各要素にアクセスできるオブジェクトです。
また、コンストラクタで使われるのは以下のプロパティのようです。

- `0`: キー
- `1`: 値

そのためコンストラクタの引数`iterable`は、この2つのプロパティがあるオブジェクトを要素に取れます。

例えば以下のようなオブジェクトでも正常に`Map`を作成できます。

```js
new Map([{ 0: 'key', 1: 'value' }])
// Map(1) { 'key' => 'value' }
```

:::note
仕様書だと[`Map`](https://tc39.es/ecma262/multipage/keyed-collections.html#sec-map-iterable)から辿れる[`AddEntriesFromIterable`](https://tc39.es/ecma262/multipage/keyed-collections.html#sec-add-entries-from-iterable)が該当しそうです。

> The parameter iterable is expected to be an object that implements a %Symbol.iterator% method that returns an iterator object that produces a two element array-like object whose first element is a value that will be used as a Map key and whose second element is the value to associate with that key.

機械翻訳:
> パラメータ iterable は、最初の要素がマップ・キーとして使用される値で、2 番目の要素がそのキーに関連付けられる値である 2 つの要素の配列のようなオブジェクトを生成するイテレータ・オブジェクトを返す %Symbol.iterator% メソッドを実装するオブジェクトであることが期待されます。

また、Array-Likeについては[こちらの記事](https://qiita.com/righteous/items/56d6086ec83be08d3553)も参考になると思います。
:::

# オブジェクトとの違い

大半の場合では、通常のオブジェクトリテラルでも`Map`と似たようなことができます。
しかし、オブジェクトリテラルではなく`Map`を使うべき場面もあります。

## サイズを取得したい場合
`Map`の場合、サイズは`size`プロパティから取得できます。

```js
const map = new Map([
  [1: 'ABC'],
  [2: 'DEF'],
  [3: 'GHI'],
])

map.size // 3
```

しかし、特定のオブジェクトにいくつの要素があるのかを、直接調べる方法はありません。
`Object.keys()`から返される配列の`length`を調べる方法があります。

> `Object`内のアイテムの数を決定することは、より回りくどく、効率的ではありません。一般的な方法は、`Object.keys()`から返される配列の`length`を通じて行う方法です。 - [MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map#object_と_map_の比較)

```js
const obj = {
  1: 'ABC',
  2: 'DEF',
  3: 'GHI'
}

Object.keys(obj).length // 3
```

## ユーザーの入力を入れる場合
`Map`のキーや値にユーザーの入力があるのは**安全**です。

```js
const input = ['__proto__', 'console.log("Hello World!")']
map.set(input[0], input[1]) // OK
```

しかし、オブジェクトにユーザーの入力を入れた場合、予期せぬ挙動を引き起こす場合があります。

特にバリデーションなどを設けなかった場合、ユーザーは`__proto__`や`constructor`など、JavaScriptの内部の動作に影響を及ぼす可能性のあるプロパティを書き換えられます。
これは[オブジェクトインジェクション攻撃](https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/the-dangers-of-square-bracket-notation.md)を引き起こす可能性があります。

```js
const input = ['__proto__', 'console.log("Hello World!")']
map.set(input[0], input[1]) // OK
```

> ユーザーが提供したキーと値のペアを Object に設定すると、攻撃者がオブジェクトのプロトタイプを上書きできる可能性があり、 オブジェクトインジェクション攻撃 につながる可能性があります。

> 偶発的なキーの問題と同様に、これもnull-prototypeオブジェクトを使用することによって軽減することができます。

## シリアライズしたい場合
文字列にシリアライズしたい場合、オブジェクトのほうが簡単です。
オブジェクトは`JSON.stringify`と`JSON.parse`で簡単に文字列と変換できます。

```js
const obj = { key: 'value' }

// JSON形式の文字列にできる
const str = JSON.stringify(obj) // '{"key":"value"}'

// 簡単にオブジェクトにできる
const obj2 = JSON.parse(str) // {key: 'value'}
```

しかし、`Map`だとそう簡単にはいきません。
シリアライズしたい場合、そのような処理を自作するか、ライブラリに頼る必要があります。

> シリアライズや解釈のためのネイティブな対応はありません。

## キーの順番が重要な場合
詳細は[こちらの記事](https://qiita.com/anqooqie/items/ab3fed5d269d278cdd2b)が参考になると思います。

### `Map`の場合
キーは`set`で挿入された順番で保管されます。
そのため`forEach`メソッドなどでの反復処理の順番は、`set`で登録された順番になります。

また、コンストラクタで追加された要素の順番も保証されます。

### オブジェクトの場合
オブジェクトのプロパティの順序は複雑だそうです。
そのため、順序に依存する処理はあまり書かないほうが良いでしょう。

> 通常の Object のキーは現在では順序付けされていますが、以前はそうではなかったので、順序は複雑です。結果として、プロパティの順序に頼らない方が良いでしょう。

---

これ以外にもオブジェクトと`Map`にはいくつか違いがあります。
詳細は[MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map#object_と_map_の比較)をご覧ください。