Map・Set・WeakRef — コレクションの使いどころ
この章の目次開く
- Map — あらゆる型をキーにできる連想配列
- 基本の使い方
- キーの同値判定 — SameValueZero
- Mapの主要メソッド
- Mapはイテラブル
- Object との使い分け
- 実務でよく使うパターン
- Set — 重複しない値のコレクション
- 基本の使い方
- 重複の判定ルール — 「同じ値」とは何か
- 主要メソッド
- 実務でよく使うパターン — 配列の重複排除
- Setはイテラブル
- Setの集合演算(ES2025)
- WeakMap・WeakSet — GCに優しいコレクション
- WeakMap
- WeakMapの制約
- WeakMapの実務での使いどころ
- WeakSet
- WeakRef・FinalizationRegistry — 弱参照と後始末
- ちゃんと使うためのポイント
- 参考リンク
JavaScriptの仕様では、Map・Set・WeakMap・WeakSet は Keyed Collections(キー付きコレクション)として整理されています。 MDNのJavaScriptガイドにも キー付きコレクション という章があります。
この章で扱うコレクションとは、複数の値、またはキーと値のペアをまとめて保持するためのオブジェクトです。 Mapはキーと値のペアを保持し、Setは重複しない値の集まりを保持します。 ES2015で追加されたMapとSetは、「任意の値をキーにできる」「重複しない値だけを保持する」といった、 従来のオブジェクト・配列では扱いにくかった要件を明確なAPIで解決します。
この章ではMap・Set・WeakMap・WeakSet・WeakRefの順に、それぞれの特性と実務での使いどころを整理します。
学習者オブジェクトがあるのに Map って必要なの?Set も配列で代用できそうだけど…何が違うの?
Map — あらゆる型をキーにできる連想配列
Mapは キーと値のペアを格納する コレクションです。
一見するとオブジェクト({})と同じに思えますが、決定的な違いがあります。
オブジェクトのキーは文字列(またはSymbol)に限定されるのに対し、Mapは オブジェクト・関数・数値など、あらゆる型をキーにできます。
他に個人的にポイントとなる特徴は、便利なメソッド(get、set、has、delete、clear、forEach)があることです。
基本の使い方

const m = new Map();
m.set('name', 'Alice');
m.set(42, 'the answer');
m.set(true, 'yes');
console.log(m.get('name')); // "Alice"
console.log(m.get(42)); // "the answer"
console.log(m.size); // 3コンストラクタに [key, value] ペアの配列を渡して初期化することもできます。
const config = new Map([
['theme', 'dark'],
['lang', 'ja'],
['fontSize', 16],
]);
console.log(config.get('lang')); // "ja"
学習者オブジェクトをキーにできるって、具体的にどういうこと?
たとえばDOM要素をキーにして、その要素に紐づくデータを管理できます。
const metadata = new Map();
const btn = document.querySelector('#submit');
metadata.set(btn, { clickCount: 0, lastClicked: null });
// ボタンがクリックされたら
metadata.get(btn).clickCount++;オブジェクトでこれをやろうとすると、キーが文字列化されて "[object HTMLButtonElement]" になってしまい、まったく区別がつきません。
キーの同値判定 — SameValueZero

MapのキーもSetと同じく SameValueZero アルゴリズムで比較されます。
学習者SameValueZero って何ですか?
実際に押さえておくべき点は一つだけ——NaN 同士は同一とみなされる、これだけです。
const m = new Map();
m.set(NaN, 'not a number');
m.set(NaN, 'overwritten');
console.log(m.size); // 1 — NaN同士は同じキーとして扱われる
console.log(m.get(NaN)); // "overwritten"オブジェクトキーは 参照で比較 されます。見た目が同じでも別のオブジェクトは別のキーです。
const m = new Map();
m.set({ id: 1 }, 'Alice');
m.set({ id: 1 }, 'Bob');
console.log(m.size); // 2 — 別オブジェクトなので別キー同じオブジェクトをキーとして使いたい場合は、変数に保持しておく必要があります。
const key = { id: 1 };
const m = new Map();
m.set(key, 'Alice');
console.log(m.get(key)); // "Alice"Mapの主要メソッド
構文: new Map() または new Map(iterable)
| メソッド | 説明 | 戻り値 |
|---|---|---|
set(key, value) | キーと値のペアを追加・上書き | Map自身 |
get(key) | キーに対応する値を取得 | 値 or undefined |
has(key) | キーが存在するか | boolean |
delete(key) | キーと値のペアを削除 | 削除できたら true |
clear() | 全要素を削除 | undefined |
forEach(fn) | 各ペアに対してコールバック実行 | undefined |
学習者forEach って (値, キー) の順番なんですか?直感と逆な感じがして…
そうなんです、混乱しやすいポイントです。配列の forEach が (要素, インデックス) という順なので、それに揃えて「まず中身、次に識別子」という設計になっています。Map もその流れで (値, キー) の順になりました。
const m = new Map([['name', 'Alice'], ['age', 25]]);
m.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// "name: Alice"
// "age: 25"set はMap自身を返すので、メソッドチェーンが可能です。
const m = new Map().set('a', 1).set('b', 2).set('c', 3);Mapはイテラブル

Mapは for...of で回せます(このような性質を「イテラブル」と呼びます。詳しい仕組みは次章のイテレータとジェネレータで解説します)。各要素は [key, value] の配列として取り出せます。
挿入順が保持されるのはSetと同じです。
const m = new Map([
['name', 'Alice'],
['age', 25],
['city', 'Tokyo'],
]);
for (const [key, value] of m) {
console.log(`${key}: ${value}`);
}
// "name: Alice"
// "age: 25"
// "city: Tokyo"keys()・values()・entries() の各メソッドでイテレータを取得することもできます。
console.log([...m.keys()]); // ["name", "age", "city"]
console.log([...m.values()]); // ["Alice", 25, "Tokyo"]
console.log([...m.entries()]); // [["name", "Alice"], ["age", 25], ["city", "Tokyo"]]
学習者Map って .map() や .filter() は使えないんですか?
Mapは配列ではないので、そのままでは使えません。ただし、スプレッド構文や Array.from で配列に変換すれば使えます。
学習者あ、そっか…Map って配列じゃないんですね。ずっと配列みたいなものだと思ってました!
const m = new Map([['a', 1], ['b', 2], ['c', 3]]);
// values() を配列に変換してから filter
const big = [...m.values()].filter(v => v > 1);
console.log(big); // [2, 3]
// entries() を配列に変換してから map
const labels = [...m.entries()].map(([k, v]) => `${k}=${v}`);
console.log(labels); // ["a=1", "b=2", "c=3"]Object との使い分け

学習者結局、普通のオブジェクトと Map はどう使い分ければいいの?
| 観点 | Object ({}) | Map |
|---|---|---|
| キーに使える型 | 文字列・Symbol のみ | あらゆる型 |
| キーの順序 | 基本は挿入順だが数値キーが先に来る | 常に挿入順 |
| サイズの取得 | Object.keys(obj).length | map.size(O(1)) |
| イテレーション | Object.entries() 等で変換が必要 | そのまま for...of で回せる |
| プロトタイプ汚染のリスク | toString 等の既存キーと衝突する可能性 | キー空間が完全にクリーン |
| JSON との相互変換 | JSON.stringify でそのまま変換可能 | 直接変換できない(手動変換が必要) |
| パフォーマンス | 少量のデータ、静的な構造に最適化 | 頻繁な追加・削除に強い |
先生迷ったら「キーが文字列で、構造が固定なら Object」「キーが動的・非文字列、頻繁に追加削除するなら Map」と覚えると実務で困らないよ。
文字列キーでも Map を選ぶケース
学習者キーが全部文字列なら、Object で十分じゃない?わざわざ Map にする意味あるの?
キーが文字列だけでも、キーの中身が事前に決まっていない(動的な)場合はMapの方が安全で便利です。 たとえばユーザー入力やAPIレスポンスのキーをそのまま格納するケースを考えてみましょう。
// Objectだとプロトタイプのプロパティと衝突する危険がある
const obj = {};
obj['toString'] = 'oops';
// obj.toString() → TypeError!(プロトタイプのメソッドを上書きしてしまった)
// Mapならキー空間が完全にクリーン
const m = new Map();
m.set('toString', 'safe'); // 何の問題もないメソッドの使い勝手にも差があります。
const obj = { a: 1, b: 2, c: 3 };
const map = new Map([
['a', 1],
['b', 2],
['c', 3],
]);
// サイズ取得
Object.keys(obj).length; // 3(毎回キー配列を生成)
map.size; // 3(プロパティアクセスだけ、O(1))
// キーの存在確認
'a' in obj; // true(プロトタイプチェーンも見てしまう)
Object.hasOwn(obj, 'a'); // true(安全だが冗長)
map.has('a'); // true(シンプル)
// 削除
delete obj.b; // true だが、もとから無いキーでも true
map.delete('b'); // true(実際に削除できたときだけ true)実務での判断基準をまとめると次のようになります。
| シナリオ | 選択 | 理由 |
|---|---|---|
構造が固定({ name, age } のような型付きデータ) | Object | TypeScriptの型推論が効く、JSONとの親和性が高い |
| キーが動的(ユーザー入力、辞書的な用途) | Map | プロトタイプ汚染の心配がない、メソッドが便利 |
| 頻繁に追加・削除を繰り返す | Map | パフォーマンス面で有利 |
| サイズを頻繁にチェックする | Map | size がO(1)で取得できる |
先生「キーが文字列か否か」より「構造が固定か動的か」で選ぶのが実務的なコツだね。動的なキー・値ペアを扱うなら、文字列キーでもMapの方が自然だよ。
実務でよく使うパターン

Object → Map → Object の変換
APIレスポンス(オブジェクト)をMapに変換して処理し、最後にオブジェクトに戻すパターンです。
const data = { name: 'Alice', age: 25, city: 'Tokyo' };
// Object → Map
const m = new Map(Object.entries(data));
// Map → Object
const obj = Object.fromEntries(m);
console.log(obj); // { name: "Alice", age: 25, city: "Tokyo" }カウンター(出現回数の集計)
配列内の要素の出現回数をMapで集計する、実務頻出のパターンです。
const words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple'];
const counter = new Map();
for (const word of words) {
counter.set(word, (counter.get(word) ?? 0) + 1);
}
console.log(counter);
// Map(3) { "apple" => 3, "banana" => 2, "cherry" => 1 }キャッシュ・メモ化
計算コストの高い処理の結果をMapにキャッシュするパターンです。
const cache = new Map();
function expensiveCalc(input) {
if (cache.has(input)) {
return cache.get(input);
}
const result = /* 重い計算 */ input * input;
cache.set(input, result);
return result;
}Set — 重複しない値のコレクション
Setは 同じ値を1つしか持たない コレクションです。 配列と似ていますが、インデックスによるアクセスはなく、「この値が含まれているか」を高速に判定できるのが特徴です。
基本の使い方
const s = new Set();
s.add(1);
s.add(2);
s.add(1); // すでに存在するので無視される(エラーにはならない)
console.log(s); // Set(2) { 1, 2 }
console.log(s.size); // 2コンストラクタに配列を渡すと、重複を取り除いた状態で初期化されます。
const sample = new Set([1, 1, 2, 3, 3]);
console.log(sample); // Set(3) { 1, 2, 3 }注意点として、console.log の出力は配列 [1, 2, 3] ではなく Set(3) { 1, 2, 3 } です。
配列として取り出したい場合はスプレッド構文か Array.from を使います。
const arr = [...sample]; // [1, 2, 3]
const arr2 = Array.from(sample); // [1, 2, 3]重複の判定ルール — 「同じ値」とは何か
Setの同値判定は SameValueZero アルゴリズムに従います。
ほぼ ===(厳密等価)と同じですが、1つだけ違いがあります。
// === での比較
console.log(NaN === NaN); // false(!)
// Setでの比較
const s = new Set();
s.add(NaN);
s.add(NaN);
console.log(s.size); // 1 — NaN同士を「同じ値」とみなす=== では NaN は自分自身と等しくないという仕様ですが、
Setでは NaN を同一の値として扱います。
これは実用上は自然な挙動で、「NaNが際限なく追加されてしまう」といった事故を防ぎます。
もう1つ押さえておきたいのは、オブジェクトは参照で比較される という点です。
const s = new Set();
s.add({ id: 1 });
s.add({ id: 1 });
console.log(s.size); // 2 — 見た目が同じでも別オブジェクトなので重複扱いにならない見た目が同じオブジェクトでも、=== で等しくなければ別の値として追加されます。
主要メソッド
| メソッド | 説明 | 戻り値 |
|---|---|---|
add(value) | 値を追加(重複なら無視) | Set自身 |
has(value) | 値が存在するか | boolean |
delete(value) | 値を削除 | 削除できたら true |
clear() | 全要素を削除 | undefined |
forEach(fn) | 各要素に対してコールバック実行 | undefined |
add はSet自身を返すので、メソッドチェーンが可能です。
const s = new Set().add(1).add(2).add(3);実務でよく使うパターン — 配列の重複排除
const ids = [1, 3, 5, 3, 1, 7];
const unique = [...new Set(ids)]; // [1, 3, 5, 7]この1行イディオムは頻出です。APIレスポンスに重複IDが含まれる場合や、 ユーザーの選択肢から重複を取り除くときなどに使います。
Setはイテラブル
SetもMapと同じくイテラブルなので、for...of やスプレッド構文で回せます。挿入順が保持されます。
const s = new Set(['a', 'b', 'c']);
for (const v of s) {
console.log(v); // "a", "b", "c"
}Setの集合演算(ES2025)
ES2025で、数学的な集合演算メソッドが追加されました。
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.union(b); // Set { 1, 2, 3, 4 } — 和集合
a.intersection(b); // Set { 2, 3 } — 積集合(共通要素)
a.difference(b); // Set { 1 } — 差集合(aにだけある要素)
a.symmetricDifference(b); // Set { 1, 4 } — 対称差(どちらか片方にだけある要素)
a.isSubsetOf(b); // false — aはbの部分集合か
a.isSupersetOf(b); // false — aはbの上位集合か
a.isDisjointFrom(b); // false — 共通要素がないか以前はスプレッドと filter で手書きしていた処理が、ネイティブメソッドで書けるようになりました。
WeakMap・WeakSet — GCに優しいコレクション
MapやSetは、キー(Mapの場合)や値(Setの場合)への参照を持ち続けるため、格納したオブジェクトがガベージコレクション(GC)の対象にならなくなります。たとえばDOM要素をMapのキーにした場合、その要素がページから削除されても、Mapが参照を持ち続ける限りメモリ上に残ります。
WeakMap と WeakSet は、この問題を解決するための「弱い参照」版のコレクションです。
WeakMap
WeakMapはキーにオブジェクトしか使えない代わりに、キーとなるオブジェクトへの参照が**弱い(weak)**です。他のどこからもそのオブジェクトを参照しなくなると、WeakMapに入っていてもGCに回収されます。
const privateData = new WeakMap();
function createUser(name) {
const user = { name };
privateData.set(user, { loginCount: 0, lastLogin: null });
return user;
}
let alice = createUser('Alice');
privateData.get(alice); // { loginCount: 0, lastLogin: null }
alice = null; // alice への参照がなくなる
// → WeakMap内のエントリもGC時に自動的に消えるWeakMapの制約
弱い参照であるがゆえの制約があります。
| できること | できないこと |
|---|---|
get(key) / set(key, value) / has(key) / delete(key) | size の取得 |
| オブジェクトキーの使用 | for...of による列挙 |
keys() / values() / entries() | |
| 文字列・数値をキーにすること |
GCのタイミングは予測できないため、「今いくつ入っているか」を知る手段がありません。列挙もできません。
WeakMapの実務での使いどころ
学習者普通の Map を使って、不要になったら delete すればいいのでは?
先生手動で delete するのは漏れやすい。WeakMap
なら「不要になったら勝手に消える」から、メモリリークのリスクがそもそもないんだ。
| ユースケース | 説明 |
|---|---|
| DOM要素へのメタデータ付与 | 要素が削除されたら自動的にメタデータも消える |
| プライベートデータの格納 | クラスインスタンスに外部からアクセスできない情報を紐づける |
| キャッシュ | 元のオブジェクトが不要になったらキャッシュも自動解放 |
// DOM要素にメタデータを紐づける
const elementData = new WeakMap();
function trackElement(el) {
elementData.set(el, { created: Date.now(), clicks: 0 });
}
// el がDOMから削除され、他の参照もなくなれば
// WeakMap内のエントリも自動的にGC対象になるWeakSet
WeakSetはSetの弱い参照版です。オブジェクトだけを格納でき、格納したオブジェクトへの参照が弱いです。
const visited = new WeakSet();
function processOnce(obj) {
if (visited.has(obj)) return; // 処理済み
visited.add(obj);
// 処理...
}WeakSetの制約もWeakMapと同様で、size・列挙・イテレーションはできません。使いどころは「あるオブジェクトに対して処理済みかどうかのフラグ」や「循環参照の検知」などに限られます。
WeakRef・FinalizationRegistry — 弱参照と後始末
WeakRef は任意のオブジェクトへの弱い参照を作ります。WeakMapと違い、キー・値のペアではなく、単体のオブジェクトへの弱い参照です。
let target = { data: 'important' };
const ref = new WeakRef(target);
ref.deref(); // { data: 'important' } — まだ生きている
target = null;
// GC後
ref.deref(); // undefined — 回収されたFinalizationRegistry は、オブジェクトがGCで回収されたときにコールバックを実行する仕組みです。
const registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue} が回収されました`);
});
let obj = { name: 'temp' };
registry.register(obj, 'temp object');
obj = null;
// GC発生時に "temp object が回収されました" と出力される(タイミングは不定)ちゃんと使うためのポイント
- Setは同じ値を追加してもエラーにならず、単に無視される。サイレントに重複を弾く設計
- 同値判定は
===とほぼ同じだが、NaN同士は同一とみなされる(SameValueZero) - オブジェクトは参照比較。見た目が同じでも
===で等しくなければ別の値 - 配列の重複排除は
[...new Set(arr)]のワンライナーが定番 - Mapはキーにオブジェクトを使いたいとき、Setは一意性を保証したいときに選ぶ
次の章では、for...ofの裏側にあるイテレータとジェネレータの仕組みを解説します。
