JavaScriptの基礎 - Map
概要
JavaScriptのMapは、ES2015 (ES6) で導入されたキーバリューペアのコレクションである。
通常のObjectと異なり、キーとして文字列やSymbolだけでなく、オブジェクト、関数、数値等、任意の型を使用できる。
キーの比較にはSameValueZeroアルゴリズムを採用しており、NaN 同士を同一のキーとして扱うことができる。
挿入した順序が仕様によって保証されており、反復処理においても挿入順に要素が返される。
size プロパティで要素数を直接取得できるほか、forEach や for...of による反復処理、
keys() / values() / entries() によるイテレータ取得等、豊富な操作メソッドが用意されている。
ES2024では静的メソッド Map.groupBy() が追加され、配列等のイテラブルをグループ化してMapとして返す操作が標準化された。
また、弱参照版である WeakMap も用意されており、キーへの外部参照がなくなった際にガベージコレクタが自動的にエントリを削除するため、
メモリリークを防ぎつつプライベートデータの格納やDOM要素へのメタデータ付与に活用できる。
Mapの作成
Mapを作成する方法は主にコンストラクタを使用する1種類である。
ES2024からは静的メソッド Map.groupBy() によるグループ化も可能になった。
コンストラクタ
new Map() で空のMapを生成し、new Map(iterable) で初期値を持つMapを生成する。
new を省略すると TypeErrorが発生するため注意が必要である。
iterableを渡す場合、各要素は [key, value] の形式でなければならない。
// 空のMapを生成
const map1 = new Map();
// 初期値を持つMapを生成 ([key, value] のペアの配列を渡す)
const map2 = new Map([
["name", "Alice"],
["age", 30],
["city", "Tokyo"]
]);
console.log(map2.get("name")); // "Alice"
console.log(map2.size); // 3
// Objectからの変換
const obj = { a: 1, b: 2, c: 3 };
const map3 = new Map(Object.entries(obj));
console.log(map3.get("a")); // 1
// newなしではTypeError
// const map4 = Map(); // TypeError: Constructor Map requires 'new'
Map.groupBy (ES2024)
Map.groupBy(items, callback) は、イテラブルの各要素にコールバック関数を適用し、その戻り値をキー、対応する要素の配列を値とするMapを返す静的メソッドである。
2024年3月以降の全モダンブラウザで利用可能である。
コールバック関数の戻り値はキーとして使用されるため、オブジェクトを含む任意の型を返すことができる点が Object.groupBy() との違いである。
Object.groupBy() はキーが文字列に変換されるのに対し、Map.groupBy() はキーの型をそのまま保持する。
const inventory = [
{ name: "apple", type: "fruit", count: 5 },
{ name: "banana", type: "fruit", count: 0 },
{ name: "carrot", type: "vegetable", count: 2 },
{ name: "daikon", type: "vegetable", count: 10 },
];
// 文字列キーでグループ化
const byType = Map.groupBy(inventory, (item) => item.type);
console.log(byType.get("fruit"));
// [ { name: "apple", ... }, { name: "banana", ... } ]
// オブジェクトをキーとしてグループ化 (Map.groupByのみ可能)
const restock = { restock: true };
const sufficient = { restock: false };
const byRestock = Map.groupBy(inventory, (item) =>
item.count > 0 ? sufficient : restock
);
console.log(byRestock.get(restock));
// [ { name: "banana", ... } ]
// Object.groupByとの比較 (キーは文字列に変換される)
const objGrouped = Object.groupBy(inventory, (item) => item.type);
console.log(objGrouped.fruit); // Object.groupBy はドットアクセスで取得
要素の操作
Mapには要素の追加、取得、確認、削除のためのメソッドが用意されている。
set / get
set(key, value) はMapにキーバリューペアを追加または更新するメソッドである。
Mapインスタンス自身を返すため、メソッドチェーンで連続して呼び出すことができる。
get(key) は指定したキーに対応する値を返し、キーが存在しない場合は undefined を返す。
const map = new Map();
// set : キーバリューペアを追加
map.set("name", "Alice");
map.set("age", 30);
// setはチェーン可能 (Mapインスタンス自身を返す)
map.set("city", "Tokyo").set("country", "Japan");
// get : 値を取得
console.log(map.get("name")); // "Alice"
console.log(map.get("age")); // 30
// 存在しないキーはundefinedを返す
console.log(map.get("email")); // undefined
// キーを更新する
map.set("age", 31);
console.log(map.get("age")); // 31
has / delete / clear
各メソッドの動作を以下に示す。
| メソッド | 説明 |
|---|---|
has(key) |
指定したキーがMapに存在するかを真偽値で返す。 |
delete(key) |
指定したキーとその値をMapから削除する。 削除に成功した場合はtrue、キーが存在しなかった場合はfalseを返す。 |
clear() |
Map内の全ての要素を削除する。 戻り値は、undefined である。 |
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
// has: キーの存在確認
console.log(map.has("a")); // true
console.log(map.has("z")); // false
// delete: 特定のキーを削除
console.log(map.delete("b")); // true (削除成功)
console.log(map.delete("z")); // false (存在しないキー)
console.log(map.size); // 2
// clear: 全要素を削除
map.clear();
console.log(map.size); // 0
sizeプロパティ
size は読み取り専用のプロパティで、Mapに格納されているキーバリューペアの数を返す。
Objectのサイズ取得には Object.keys(obj).length が必要であるのに対し、Mapは map.size で直接取得することができる。
const map = new Map([
["x", 10],
["y", 20],
["z", 30],
]);
console.log(map.size); // 3
map.set("w", 40);
console.log(map.size); // 4
map.delete("x");
console.log(map.size); // 3
// Objectとの比較
const obj = { x: 10, y: 20, z: 30 };
console.log(Object.keys(obj).length); // 3 (Objectはこの方法が必要)
キーの特性
Mapのキーはあらゆる型の値を使用できる点が、通常のObjectとの大きな違いである。
キーに使用できる型
Mapのキーとして使用できる型を以下に示す。
| 種別 | 内容 |
|---|---|
| プリミティブ型 | 文字列、数値、真偽値、undefined、null、Symbol、BigInt |
| オブジェクト | 配列、関数、クラスインスタンス等、参照型全般 |
NaN |
NaN 自体もキーとして使用できる。 |
const map = new Map();
// プリミティブ型のキー
map.set("string", "文字列キー");
map.set(42, "数値キー");
map.set(true, "真偽値キー");
map.set(null, "nullキー");
map.set(undefined, "undefinedキー");
map.set(Symbol("s"), "Symbolキー");
// オブジェクトをキーとして使用
const obj = { id: 1 };
const arr = [1, 2, 3];
const func = () => {};
map.set(obj, "オブジェクトキー");
map.set(arr, "配列キー");
map.set(func, "関数キー");
// NaN をキーとして使用
map.set(NaN, "NaNキー");
console.log(map.get(NaN)); // "NaNキー"
console.log(map.size); // 10
キーの同値比較
MapのキーはSameValueZeroアルゴリズムで比較される。
このアルゴリズムは厳密等価 (===) と概ね同じだが、以下の点が異なる。
NaN同士を同一視する- 厳密等価では
NaN === NaNがfalseになるが、SameValueZeroではNaNを同一キーとして扱う。
- 厳密等価では
+0と-0を同一視する- SameValueアルゴリズムでは
+0と-0を区別するが、SameValueZeroでは同一視する。
- SameValueアルゴリズムでは
- その他の値は厳密等価 (
===) と同じ比較を行う
SameValueZeroが採用された理由は、厳密等価を使用した場合に NaN のキー判定が不可能になり、
SameValueを使用した場合に +0 と -0 が別キーとして扱われ混乱を招くためである。
const map = new Map();
// NaN 同士は同一視される (厳密等価とは異なる)
map.set(NaN, "first");
map.set(NaN, "second"); // 既存のNaNキーを上書き
console.log(map.size); // 1
console.log(map.get(NaN)); // "second"
// 参考: 厳密等価では NaN は NaN と等しくない
console.log(NaN === NaN); // false
// +0 と -0 は同一視される
map.set(+0, "zero");
map.set(-0, "minus zero"); // +0 のエントリを上書き
console.log(map.size); // 2 (NaN のエントリ + 0 のエントリ)
console.log(map.get(0)); // "minus zero"
console.log(map.get(-0)); // "minus zero"
// オブジェクトキーは参照で比較される
const obj1 = { id: 1 };
const obj2 = { id: 1 }; // 内容は同じだが別オブジェクト
map.set(obj1, "obj1");
map.set(obj2, "obj2");
console.log(map.size); // 4 (obj1 と obj2 は別キー)
console.log(map.get(obj1)); // "obj1"
console.log(map.get(obj2)); // "obj2"
反復処理
Mapは挿入順に要素を反復処理するための複数の方法を提供している。
forEach
forEach(callback) は、Mapの各エントリに対してコールバック関数を呼び出す。
コールバック関数の引数は (value, key, map) の順である。
Arrayの forEach と異なり、第1引数がvalueであることに注意が必要である。
const map = new Map([
["name", "Alice"],
["age", 30],
["city", "Tokyo"],
]);
map.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// name: Alice
// age: 30
// city: Tokyo
// 第3引数でMapインスタンス自体を受け取れる
map.forEach((value, key, m) => {
console.log(m.size); // 3 (毎回出力)
});
for...of
for...of を使用してMapを反復処理できる。
MapのデフォルトイテレータはEntries (キーバリューペアの配列) を返すため、for...of でそのまま分割代入が使用できる。
const map = new Map([
["name", "Alice"],
["age", 30],
["city", "Tokyo"],
]);
// デフォルトイテレータは entries() と同等
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
// name: Alice
// age: 30
// city: Tokyo
keys / values / entries
Mapは3種類のイテレータメソッドを提供している。
| メソッド | 説明 |
|---|---|
keys() |
挿入順にキーを返すイテレータを生成する。 |
values() |
挿入順に値を返すイテレータを生成する。 |
entries() |
挿入順に [key, value] のペアを返すイテレータを生成する。Mapのデフォルトイテレータと同等 |
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
// keys() : キーのイテレータ
for (const key of map.keys()) {
console.log(key); // "a", "b", "c"
}
// values() : 値のイテレータ
for (const value of map.values()) {
console.log(value); // 1, 2, 3
}
// entries() : [key, value]ペアのイテレータ
for (const [key, value] of map.entries()) {
console.log(`${key} => ${value}`); // "a => 1", "b => 2", "c => 3"
}
配列への変換
Mapは Array.from() または スプレッド構文 (...) を使用して配列に変換できる。
const map = new Map([
["x", 10],
["y", 20],
["z", 30],
]);
// Array.from()で変換
const entries1 = Array.from(map);
console.log(entries1); // [["x", 10], ["y", 20], ["z", 30]]
const keys1 = Array.from(map.keys());
const values1 = Array.from(map.values());
console.log(keys1); // ["x", "y", "z"]
console.log(values1); // [10, 20, 30]
// スプレッド構文で変換
const entries2 = [...map];
const keys2 = [...map.keys()];
const values2 = [...map.values()];
console.log(entries2); // [["x", 10], ["y", 20], ["z", 30]]
// Mapにfilterやmap等の配列メソッドを適用する場合
const filtered = [...map].filter(([key, value]) => value > 10);
console.log(filtered); // [["y", 20], ["z", 30]]
Object との比較
MapとObjectはどちらもキーバリューペアを格納できるが、特性や用途に違いがある。
| 観点 | Map | Object |
|---|---|---|
| キーの型 | 任意の型 (オブジェクト、関数、NaN等を含む) | 文字列、Symbol のみ |
| 挿入順の保持 | 仕様で保証 | 実装依存 (整数キーは昇順、その他は挿入順が多い) |
| サイズ取得 | map.size で直接取得 |
Object.keys(obj).length が必要
|
| 反復処理 | for...of、forEach、各種イテレータが利用可能 |
for...in、Object.keys() 等が必要
|
| パフォーマンス | 追加・削除・反復処理が高速 (特に大規模データ) | 小規模データの読み取り中心では有利な場合がある。 |
| JSON変換 | 非対応 (カスタム変換が必要) | JSON.stringify() / JSON.parse() で直接対応
|
| プロトタイプ | なし (意図しないキーの衝突リスクが低い) | Object.prototype を継承 (意図しないキー衝突の可能性あり)
|
使い分けの指針
| 型 | 場面 | 理由 |
|---|---|---|
| Map | キーの追加・削除が頻繁に発生する場合 | Mapはdeleteが高速であり、大量のエントリを扱う場合も効率的である。 |
| キーとして文字列・Symbol以外を使用したい場合 | オブジェクトや関数、NaN等をキーとして使用する場合はMapが必要である。 | |
| 挿入順を確実に保持する必要がある場合 | Objectの挿入順保持はブラウザ依存であるため、仕様で保証されているMapを使用する。 | |
| 大規模データの反復処理が必要な場合 | Mapの反復処理はObjectより高速な場合があり、特に大規模データで顕著である。 | |
| Object | JSONとの相互変換が必要な場合 | JSON.stringify() / JSON.parse() はObjectを直接サポートする。
|
| 構造が固定されたレコードを扱う場合 | 固定フィールドを持つデータ構造にはObjectの方が直感的で読みやすい。 | |
| 小規模でキーが文字列の場合 | シンプルなキーバリューペアであればObjectの方が記述が簡潔になる場合がある。 |
WeakMap
WeakMapは、キーを弱参照で保持するMap類似のコレクションである。
WeakMapとは
WeakMapはMapと異なり、キーをガベージコレクタが回収できる弱参照で保持する。
キーへの外部参照がなくなると、ガベージコレクタが自動的に対応するエントリを削除するため、メモリリークを防ぐことができる。
WeakMapには以下の制限がある。
- キーはオブジェクトまたは未登録のSymbolのみ使用できる
- プリミティブ型 (文字列、数値等) はキーとして使用できない。
- 反復処理ができない
keys()、values()、entries()、forEach()は存在しない。
sizeプロパティがない- 格納されている要素数を取得する方法がない。
clear()メソッドがない- 全要素を一括削除する方法がない。
基本操作
WeakMapで使用できるメソッドは set、get、has、delete の4つのみである。
const weakMap = new WeakMap();
const key1 = { id: 1 };
const key2 = { id: 2 };
// set : エントリを追加
weakMap.set(key1, "value1");
weakMap.set(key2, "value2");
// get : 値を取得
console.log(weakMap.get(key1)); // "value1"
console.log(weakMap.get(key2)); // "value2"
// has : キーの存在確認
console.log(weakMap.has(key1)); // true
// delete : エントリを削除
weakMap.delete(key1);
console.log(weakMap.has(key1)); // false
// プリミティブ型はキーとして使用不可
// weakMap.set("string", "value"); // TypeError
// weakMap.set(42, "value"); // TypeError
使用例
WeakMapは主に以下の2つのパターンで活用される。
プライベートデータの格納
WeakMapを使うことで、クラスインスタンスに紐づいたプライベートデータを外部から隠蔽できる。
ES2022以降は # プライベートフィールドが推奨されるが、WeakMapパターンは依然として有用な場面がある。
// WeakMap を使ったプライベートデータパターン
const privateData = new WeakMap();
class Person {
constructor(name, age) {
privateData.set(this, { name, age });
}
getName() {
return privateData.get(this).name;
}
getAge() {
return privateData.get(this).age;
}
greet() {
const { name, age } = privateData.get(this);
return `Hello, I'm ${name}, ${age} years old.`;
}
}
const alice = new Person("Alice", 30);
console.log(alice.getName()); // "Alice"
console.log(alice.greet()); // "Hello, I'm Alice, 30 years old."
// privateData は外部からアクセスできない
// alice インスタンスが参照されなくなると、WeakMap のエントリも自動削除される
DOM要素へのメタデータ付与
WeakMapを使用して、DOM要素にメタデータを安全に付与できる。
DOM要素がドキュメントから削除されると、WeakMapのエントリも自動的にガベージコレクトされるため、メモリリークが発生しない。
const elementMetadata = new WeakMap();
function registerElement(element, metadata) {
elementMetadata.set(element, metadata);
}
function getMetadata(element) {
return elementMetadata.get(element);
}
// DOM要素にメタデータを付与
const button = document.querySelector("#myButton");
registerElement(button, {
clickCount: 0,
createdAt: Date.now(),
label: "送信ボタン",
});
button.addEventListener("click", () => {
const meta = getMetadata(button);
meta.clickCount++;
console.log(`クリック回数: ${meta.clickCount}`);
});
// button 要素が DOM から削除されると、WeakMap のエントリも自動削除される
// button.remove();
以下に、MapとWeakMapの主な違いをまとめる。
| 観点 | Map | WeakMap |
|---|---|---|
| キーの型 | 任意の型 | オブジェクト、未登録Symbol のみ |
| 反復処理 | 可能 (forEach, for...of 等) | 不可能 |
| size プロパティ | あり | なし |
| clear メソッド | あり | なし |
| GC連動 | なし (強参照) | あり (弱参照) |
| 主な用途 | 一般的なキーバリューストア | プライベートデータ、DOM メタデータ、キャッシュ |
関連情報
- JavaScriptの基礎 - Set
- Set、WeakSet、ES2025集合演算
- JavaScriptの基礎 - イテレータとジェネレータ
- イテレータプロトコル、ジェネレータ、ES2025 Iterator Helpers
- JavaScriptの基礎 - オブジェクトリテラル
- オブジェクトの基本操作、プロパティアクセス
- JavaScriptの基礎 - 配列の反復メソッド
- forEach, map, filter, reduce等の反復処理メソッド