JavaScriptの基礎 - Map

提供: MochiuWiki : SUSE, EC, PCB

📢 Webサイト閉鎖と移転のお知らせ
このWebサイトは2026年9月に閉鎖いたします。
新しい記事は移転先で追加しております。(旧サイトでは記事を追加しておりません)

概要

JavaScriptのMapは、ES2015 (ES6) で導入されたキーバリューペアのコレクションである。
通常のObjectと異なり、キーとして文字列やSymbolだけでなく、オブジェクト、関数、数値等、任意の型を使用できる。

キーの比較にはSameValueZeroアルゴリズムを採用しており、NaN 同士を同一のキーとして扱うことができる。
挿入した順序が仕様によって保証されており、反復処理においても挿入順に要素が返される。

size プロパティで要素数を直接取得できるほか、forEachfor...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 / 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のキーとして使用できる型を以下に示す。

Mapのキーに使用できる型
種別 内容
プリミティブ型 文字列、数値、真偽値、undefinednull、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 === NaNfalse になるが、SameValueZeroでは NaN を同一キーとして扱う。

  • +0-0 を同一視する
    SameValueアルゴリズムでは +0-0 を区別するが、SameValueZeroでは同一視する。
  • その他の値は厳密等価 (===) と同じ比較を行う


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種類のイテレータメソッドを提供している。

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 の比較
観点 Map Object
キーの型 任意の型 (オブジェクト、関数、NaN等を含む) 文字列、Symbol のみ
挿入順の保持 仕様で保証 実装依存 (整数キーは昇順、その他は挿入順が多い)
サイズ取得 map.size で直接取得 Object.keys(obj).length が必要
反復処理 for...offorEach、各種イテレータが利用可能 for...inObject.keys() 等が必要
パフォーマンス 追加・削除・反復処理が高速 (特に大規模データ) 小規模データの読み取り中心では有利な場合がある。
JSON変換 非対応 (カスタム変換が必要) JSON.stringify() / JSON.parse() で直接対応
プロトタイプ なし (意図しないキーの衝突リスクが低い) Object.prototype を継承 (意図しないキー衝突の可能性あり)


使い分けの指針

MapとObjectの使い分け
場面 理由
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で使用できるメソッドは setgethasdelete の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の比較
観点 Map WeakMap
キーの型 任意の型 オブジェクト、未登録Symbol のみ
反復処理 可能 (forEach, for...of 等) 不可能
size プロパティ あり なし
clear メソッド あり なし
GC連動 なし (強参照) あり (弱参照)
主な用途 一般的なキーバリューストア プライベートデータ、DOM メタデータ、キャッシュ



関連情報