本文へスキップ
ウェブエンジニア問題集
第17章

Map・Set・WeakRef — コレクションの使いどころ

約19分
この章の目次開く

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、keys、values、entries)があることです。

基本の使い方

Mapにデータを格納するイメージ
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
js

コンストラクタに [key, value] ペアの配列を渡して初期化することもできます。

const config = new Map([
  ['theme', 'dark'],
  ['lang', 'ja'],
  ['fontSize', 16],
]);
 
console.log(config.get('lang')); // "ja"
js
学習者学習者

オブジェクトをキーにできるって、具体的にどういうこと?

たとえばDOM要素をキーにして、その要素に紐づくデータを管理できます。

const metadata = new Map();
 
const btn = document.querySelector('#submit');
metadata.set(btn, { clickCount: 0, lastClicked: null });
 
// ボタンがクリックされたら
metadata.get(btn).clickCount++;
js

オブジェクトでこれをやろうとすると、キーが文字列化されて "[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"
js

オブジェクトキーは 参照で比較 されます。見た目が同じでも別のオブジェクトは別のキーです。

const m = new Map();
m.set({ id: 1 }, 'Alice');
m.set({ id: 1 }, 'Bob');
console.log(m.size); // 2 — 別オブジェクトなので別キー
js

同じオブジェクトをキーとして使いたい場合は、変数に保持しておく必要があります。

const key = { id: 1 };
const m = new Map();
m.set(key, 'Alice');
console.log(m.get(key)); // "Alice"
js

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
keys()キーのイテレータを取得イテレータ
values()値のイテレータを取得イテレータ
entries()ペアのイテレータを取得イテレータ
学習者学習者

forEach って (値, キー) の順番なんですか?直感と逆な感じがして…

そうなんです、混乱しやすいポイントです。配列の forEach が (要素, インデックス) という順なので、それに揃えて「まず中身、次に識別子」という設計になっています。Map もその流れで (値, キー) の順になりました。

const m = new Map([['name', 'Alice'], ['age', 25]]);
 
m.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});
// "name: Alice"
// "age: 25"
js

set はMap自身を返すので、メソッドチェーンが可能です。

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

Mapはイテラブル

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"
js

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"]]
js
学習者学習者

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"]
js

Object との使い分け

ObjectとMapの使い分けを考えるイメージ
学習者学習者

結局、普通のオブジェクトと Map はどう使い分ければいいの?

観点Object ({})Map
キーに使える型文字列・Symbol のみあらゆる型
キーの順序基本は挿入順だが数値キーが先に来る常に挿入順
サイズの取得Object.keys(obj).lengthmap.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'); // 何の問題もない
js

メソッドの使い勝手にも差があります。

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)
js

実務での判断基準をまとめると次のようになります。

シナリオ選択理由
構造が固定({ name, age } のような型付きデータ)ObjectTypeScriptの型推論が効く、JSONとの親和性が高い
キーが動的(ユーザー入力、辞書的な用途)Mapプロトタイプ汚染の心配がない、メソッドが便利
頻繁に追加・削除を繰り返すMapパフォーマンス面で有利
サイズを頻繁にチェックするMapsize がO(1)で取得できる
先生先生

「キーが文字列か否か」より「構造が固定か動的か」で選ぶのが実務的なコツだね。動的なキー・値ペアを扱うなら、文字列キーでもMapの方が自然だよ。

実務でよく使うパターン

実務で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" }
js

カウンター(出現回数の集計)

配列内の要素の出現回数を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 }
js

キャッシュ・メモ化

計算コストの高い処理の結果を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;
}
js

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
js

コンストラクタに配列を渡すと、重複を取り除いた状態で初期化されます。

const sample = new Set([1, 1, 2, 3, 3]);
console.log(sample); // Set(3) { 1, 2, 3 }
js

注意点として、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]
js

重複の判定ルール — 「同じ値」とは何か

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同士を「同じ値」とみなす
js

=== では NaN は自分自身と等しくないという仕様ですが、 Setでは NaN を同一の値として扱います。 これは実用上は自然な挙動で、「NaNが際限なく追加されてしまう」といった事故を防ぎます。

もう1つ押さえておきたいのは、オブジェクトは参照で比較される という点です。

const s = new Set();
s.add({ id: 1 });
s.add({ id: 1 });
console.log(s.size); // 2 — 見た目が同じでも別オブジェクトなので重複扱いにならない
js

見た目が同じオブジェクトでも、=== で等しくなければ別の値として追加されます。

主要メソッド

メソッド説明戻り値
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);
js

実務でよく使うパターン — 配列の重複排除

const ids = [1, 3, 5, 3, 1, 7];
const unique = [...new Set(ids)]; // [1, 3, 5, 7]
js

この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"
}
js

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 — 共通要素がないか
js

以前はスプレッドと 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時に自動的に消える
js

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対象になる
js

WeakSet

WeakSetはSetの弱い参照版です。オブジェクトだけを格納でき、格納したオブジェクトへの参照が弱いです。

const visited = new WeakSet();
 
function processOnce(obj) {
  if (visited.has(obj)) return; // 処理済み
  visited.add(obj);
  // 処理...
}
js

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 — 回収された
js

FinalizationRegistry は、オブジェクトがGCで回収されたときにコールバックを実行する仕組みです。

const registry = new FinalizationRegistry((heldValue) => {
  console.log(`${heldValue} が回収されました`);
});
 
let obj = { name: 'temp' };
registry.register(obj, 'temp object');
obj = null;
// GC発生時に "temp object が回収されました" と出力される(タイミングは不定)
js

ちゃんと使うためのポイント

  • Setは同じ値を追加してもエラーにならず、単に無視される。サイレントに重複を弾く設計
  • 同値判定は === とほぼ同じだが、NaN 同士は同一とみなされる(SameValueZero)
  • オブジェクトは参照比較。見た目が同じでも === で等しくなければ別の値
  • 配列の重複排除は [...new Set(arr)] のワンライナーが定番
  • Mapはキーにオブジェクトを使いたいとき、Setは一意性を保証したいときに選ぶ

次の章では、for...ofの裏側にあるイテレータとジェネレータの仕組みを解説します。

参考リンク

JavaScriptクイズに挑戦するこの章で学んだJavaScriptの知識を、4択クイズでアウトプットして定着させよう