Mapは「どんな値でもキーにできる、順番つきのキーと値の組の集まり」、Setは「重複しない値の集まり」です。 どちらもES2015から使える組み込みオブジェクトで、Mapは辞書・キャッシュ・集計に、Setは重複除去・存在確認・集合演算に向いています。2024年からはSetに union(和集合)や intersection(共通部分)などの集合演算メソッドが加わり、Mapには Map.groupBy でグループ分けする方法も加わりました。

この記事では、MapとSetの全メソッドの早見表、Setの新メソッド7つとMap.groupBy・WeakMap、Object・配列との使い分け表を用意し、すべてのコードをNode.js 22(v22.22.2)で実行した出力を載せています。新メソッドがNode 22とChromium 141で使えるかを実際に確かめた対応表、ObjectとMap・配列とSetの追加・検索速度の実測、重複除去・出現回数カウント・LRUキャッシュ・配列の差分といったコピペできるレシピ、「オブジェクトをキーにすると見つからない」「JSON.stringifyで {} になる」といったつまずきの直し方まで扱います。オブジェクトや配列そのものの操作は、別記事「JavaScriptオブジェクト操作まとめ」「JavaScript 配列 操作 一覧」で扱っているため、ここではMapとSetに絞ります。

Map・Setのメソッド早見表

やりたいこと Map Set 戻り値・ポイント
作る new Map([["a", 1]]) new Set([1, 2]) 反復可能な値を渡せる
追加・更新 map.set(key, value) set.add(value) 自分自身を返すのでつなげられる
値を取り出す map.get(key) (なし) ないキーは undefined
あるか調べる map.has(key) set.has(value) true / false
1つ削除 map.delete(key) set.delete(value) 削除できたら true
全部削除 map.clear() set.clear() undefined
件数 map.size set.size プロパティ(() をつけない)
1件ずつ処理 map.forEach((v, k) => …) set.forEach((v) => …) 引数は値が先
キーの一覧 map.keys() set.keys() イテレーター。配列は [...]
値の一覧 map.values() set.values() Setは keys() と同じ
組の一覧 map.entries() set.entries() Setは [v, v]
for...of で回す for (const [k, v] of map) for (const v of set) 追加した順
配列に変換 [...map] / Array.from(map) [...set]
Objectと相互変換 new Map(Object.entries(o)) / Object.fromEntries(map) -
和集合・共通部分・差 - a.union(b) / a.intersection(b) / a.difference(b) 新しいSetを返す(Node 22以降)
含む・含まれる - a.isSubsetOf(b) / a.isSupersetOf(b) / a.isDisjointFrom(b) 真偽値(Node 22以降)
グループ分け Map.groupBy(arr, fn) - キーに何でも使える(Node 21以降)
なければ入れて取得 map.getOrInsert(k, v) - Baseline 2026。Node 22は未対応
キーを弱く持つ WeakMap WeakSet キーはオブジェクトのみ、反復不可

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript Map・Set|使い方とObject・配列との違い一覧」

Mapの使い方(作成・追加・取得・削除・ループ)

Mapは set で入れて get で取り出し、件数は size で数えます。 Objectと違い、数値や真偽値、オブジェクトもそのままの型でキーになります。

// basic_map.mjs  (node basic_map.mjs で実行)
const stock = new Map();
stock.set("pen", 10).set("note", 3);   // set はMap自身を返すのでつなげられる
stock.set(1, "数値キー");
stock.set(true, "真偽値キー");
console.log(stock.get("pen"));      // 10
console.log(stock.get("eraser"));   // undefined
console.log(stock.has("note"));     // true
console.log(stock.size);            // 4
console.log(stock.delete("note"));  // true
console.log(stock.delete("note"));  // false(もうない)
console.log([...stock.keys()]);
console.log([...stock.values()]);
console.log([...stock.entries()]);
stock.forEach((value, key) => console.log(key, "=>", value));
for (const [key, value] of stock) console.log(`${key}: ${value}`);

const fromArr = new Map([["a", 1], ["b", 2]]);
const fromObj = new Map(Object.entries({ x: 10, y: 20 }));
console.log(fromArr, fromObj);
console.log(Object.fromEntries(fromObj));
stock.clear();
console.log(stock.size);

実行結果(Node.js v22.22.2):

10
undefined
true
4
true
false
[ 'pen', 1, true ]
[ 10, '数値キー', '真偽値キー' ]
[ [ 'pen', 10 ], [ 1, '数値キー' ], [ true, '真偽値キー' ] ]
pen => 10
1 => 数値キー
true => 真偽値キー
pen: 10
1: 数値キー
true: 真偽値キー
Map(2) { 'a' => 1, 'b' => 2 } Map(2) { 'x' => 10, 'y' => 20 }
{ x: 10, y: 20 }
0

ポイントは3つです。

  • keys()・values()・entries() は配列ではなくイテレーター(順に1つずつ取り出せるもの)を返します。配列のメソッドを使いたいときは [...map.keys()] のように展開します。
  • forEach のコールバックは (値, キー) の順です。for...of の分割代入 [key, value] とは逆なので注意します。
  • 順番は追加した順で固定です。既にあるキーを set し直しても位置は変わりません。

Setの使い方(追加・存在確認・削除)

Setは同じ値を1つしか持たない集まりで、add で入れて has で調べます。 重複は自動で無視されます。

// basic_set.mjs
const tags = new Set(["js", "css"]);
tags.add("html").add("js");      // 重複は無視される
console.log(tags);
console.log(tags.size);
console.log(tags.has("css"));
console.log(tags.delete("css"));
console.log([...tags]);
console.log([...tags.keys()], [...tags.values()]);
console.log([...tags.entries()]);
tags.forEach((value, sameValue, set) => console.log(value, sameValue, set === tags));
const nums = new Set([1, "1", NaN, NaN, 0, -0]);
console.log(nums);
tags.clear();
console.log(tags.size);

実行結果(Node.js v22.22.2):

Set(3) { 'js', 'css', 'html' }
3
true
true
[ 'js', 'html' ]
[ 'js', 'html' ] [ 'js', 'html' ]
[ [ 'js', 'js' ], [ 'html', 'html' ] ]
js js true
html html true
Set(4) { 1, '1', NaN, 0 }
0

同じ値かどうかは「SameValueZero」という規則で判定されます。 1 と "1" は型が違うので別、NaN 同士は同じ、0 と -0 は同じです。=== との違いは NaN を同じとみなす点だけです。Setの keys() と values() が同じ結果になり、entries() が [値, 値] になるのは、Mapと同じ形で扱えるようにするためです。

Setの新メソッド(union・intersection・differenceなど)

ES2025で、Setに集合演算のメソッドが7つ加わりました。 Node.js 22とChromium 141では、追加の設定なしで使えることを確認しました。どれも元のSetは変えずに新しいSet(または真偽値)を返します。

メソッド 意味 a = {pen, note, ruler}、b = {note, ruler, tape} のとき
a.union(b) 和集合(どちらかにある) {pen, note, ruler, tape}
a.intersection(b) 共通部分(両方にある) {note, ruler}
a.difference(b) 差(aにだけある) {pen}
a.symmetricDifference(b) 対称差(片方にだけある) {pen, tape}
a.isSubsetOf(b) aがbに全部含まれるか 真偽値
a.isSupersetOf(b) aがbを全部含むか 真偽値
a.isDisjointFrom(b) 共通がひとつもないか 真偽値
// setops.mjs
const a = new Set(["pen", "note", "ruler"]);
const b = new Set(["note", "ruler", "tape"]);
console.log("union               ", a.union(b));
console.log("intersection        ", a.intersection(b));
console.log("difference          ", a.difference(b));
console.log("symmetricDifference ", a.symmetricDifference(b));
const small = new Set(["pen"]);
console.log("isSubsetOf          ", small.isSubsetOf(a));
console.log("isSupersetOf        ", a.isSupersetOf(small));
console.log("isDisjointFrom      ", small.isDisjointFrom(b));
console.log("元のaは変わらない   ", a);
// 引数はSetのような値(size・has・keys を持つ)。Mapも渡せる
const m = new Map([["pen", 1], ["tape", 2]]);
console.log("Mapと共通部分       ", a.intersection(m));
try { a.union(["tape"]); } catch (e) { console.log(e.constructor.name + ": " + e.message); }
console.log("配列はSetに包む     ", a.union(new Set(["tape"])));

実行結果(Node.js v22.22.2):

union                Set(4) { 'pen', 'note', 'ruler', 'tape' }
intersection         Set(2) { 'note', 'ruler' }
difference           Set(1) { 'pen' }
symmetricDifference  Set(2) { 'pen', 'tape' }
isSubsetOf           true
isSupersetOf         true
isDisjointFrom       true
元のaは変わらない    Set(3) { 'pen', 'note', 'ruler' }
Mapと共通部分        Set(1) { 'pen' }
TypeError: The .size property is NaN
配列はSetに包む      Set(4) { 'pen', 'note', 'ruler', 'tape' }

引数に配列をそのまま渡すと TypeError: The .size property is NaN になります。 引数は「size・has・keys を持つSetのような値」と決められているためです。配列は new Set(arr) で包んでから渡します。逆に、Mapは条件を満たすのでそのまま渡せます(キーが比べられます)。

新メソッドの対応状況(実測)

機能 Node.js 22.22.2 Chromium 141(Playwright) 備考
Setの集合演算7メソッド 使える 使える ES2025。Node 20には未搭載
Map.groupBy / Object.groupBy 使える 使える ES2024。Node 21以降
イテレーターヘルパー(map().filter().toArray()) 使える 使える ES2025
Map.prototype.getOrInsert / getOrInsertComputed undefined undefined MDNでBaseline 2026。新しいブラウザ向け

確認に使ったコードは次のとおりです(Node側)。

// support.mjs
console.log(process.version);
const names = ["union","intersection","difference","symmetricDifference","isSubsetOf","isSupersetOf","isDisjointFrom"];
for (const n of names) console.log("Set.prototype."+n, typeof Set.prototype[n]);
console.log("Map.groupBy", typeof Map.groupBy, "Object.groupBy", typeof Object.groupBy);
console.log("Map.prototype.getOrInsert", typeof Map.prototype.getOrInsert, "getOrInsertComputed", typeof Map.prototype.getOrInsertComputed);
console.log("Iterator.prototype.map", typeof Iterator?.prototype?.map, "Iterator.prototype.toArray", typeof Iterator?.prototype?.toArray);
v22.22.2
Set.prototype.union function
Set.prototype.intersection function
Set.prototype.difference function
Set.prototype.symmetricDifference function
Set.prototype.isSubsetOf function
Set.prototype.isSupersetOf function
Set.prototype.isDisjointFrom function
Map.groupBy function Object.groupBy function
Map.prototype.getOrInsert undefined getOrInsertComputed undefined
Iterator.prototype.map function Iterator.prototype.toArray function

getOrInsert が使えない環境では、has・get・set の3行で同じことが書けます(後半の「グループ化」レシピを参照)。

Map.groupByでグループ分けする

Map.groupBy(配列, 関数) は、関数が返した値をキーにして要素をまとめたMapを返します。 Object.groupBy との違いは、キーに文字列以外(オブジェクトや数値)をそのまま使える点です。

// groupby.mjs
const orders = [
  { id: 1, item: "pen", status: "shipped" },
  { id: 2, item: "note", status: "pending" },
  { id: 3, item: "tape", status: "shipped" },
];
const byStatus = Map.groupBy(orders, (o) => o.status);
console.log(byStatus);
console.log(byStatus.get("shipped").map((o) => o.id));
// キーにオブジェクトを使えるのがObject.groupByとの違い
const vip = { name: "VIP" }, normal = { name: "通常" };
const members = [{ n: "A", p: 1200 }, { n: "B", p: 300 }, { n: "C", p: 5000 }];
const byRank = Map.groupBy(members, (m) => (m.p >= 1000 ? vip : normal));
console.log(byRank.get(vip).map((m) => m.n), byRank.get(normal).map((m) => m.n));
console.log(Object.groupBy(orders, (o) => o.status));

実行結果(Node.js v22.22.2):

Map(2) {
  'shipped' => [
    { id: 1, item: 'pen', status: 'shipped' },
    { id: 3, item: 'tape', status: 'shipped' }
  ],
  'pending' => [ { id: 2, item: 'note', status: 'pending' } ]
}
[ 1, 3 ]
[ 'A', 'C' ] [ 'B' ]
[Object: null prototype] {
  shipped: [
    { id: 1, item: 'pen', status: 'shipped' },
    { id: 3, item: 'tape', status: 'shipped' }
  ],
  pending: [ { id: 2, item: 'note', status: 'pending' } ]
}
選び方 Object.groupBy Map.groupBy
キーの型 文字列(他は文字列に変換) 何でもそのまま
結果の使い方 result.shipped / JSONにしやすい result.get("shipped") / 件数は size
向いている場面 結果をそのままAPIで返す・画面に出す 続けて集計する・オブジェクトでまとめる

ObjectとMapの違い・使い分け

「決まった項目を持つデータ(レコード)」はObject、「あとから増減するキーと値の辞書」はMap、と考えると迷いません。

比べる点 Object Map
キーの型 文字列とSymbol(数値は文字列になる) 何でも(オブジェクト・関数・NaNも可)
順番 整数キーは数値順 → 文字列キーは追加順 すべて追加順
件数 Object.keys(o).length(毎回配列を作る) map.size
ループ Object.entries(o) を経由 そのまま for...of
継承したキー toString などが in で見える なし(空のMapは本当に空)
外部入力をキーにする __proto__ などに注意が要る そのまま安全に入れられる
JSON化 JSON.stringify でそのまま {} になる。変換が必要
頻繁な追加・削除 得意ではない 得意(仕様上も想定されている)
決まったキーの読み取り速度 速い やや遅い(下の実測)
書きやすさ o.price、分割代入、スプレッド get/set を使う
TypeScriptの型 { id: number; name: string } Map<string, number>

プロトタイプ汚染との関係

Objectは Object.prototype を継承しているため、外部から来た文字列をキーにすると思わぬ動きをすることがあります。 次の例は、単語の出現回数をObjectで数えたときと、外部JSONのキーをそのままコピーしたときの動きです。

// proto.mjs
const counts = {};
for (const w of ["toString", "constructor"]) counts[w] = (counts[w] || 0) + 1;
console.log(counts);
const input = JSON.parse('{"__proto__": {"isAdmin": true}}');
const target = {};
for (const [k, v] of Object.entries(input)) target[k] = v;  // 単純コピー
console.log(target.isAdmin, ({}).isAdmin);
const mapCounts = new Map();
for (const w of ["toString", "constructor"]) mapCounts.set(w, (mapCounts.get(w) ?? 0) + 1);
console.log(mapCounts);
const safe = new Map(Object.entries(input));
console.log(safe.get("__proto__"), safe.get("isAdmin"));
console.log("toString" in {}, new Map().has("toString"));

実行結果(Node.js v22.22.2):

{
  toString: 'function toString() { [native code] }1',
  constructor: 'function Object() { [native code] }1'
}
true undefined
Map(2) { 'toString' => 1, 'constructor' => 1 }
{ isAdmin: true } undefined
true false

Objectで数えると、継承した関数に 1 が文字列連結されてしまいます。 また target["__proto__"] = … はプロパティの追加ではなくプロトタイプの差し替えになり、target.isAdmin が true になりました(この例では target だけが影響を受け、{} 全体には広がっていません)。Mapにすると、どちらも普通のキーとして扱われ、意図どおりに動きます。 Objectを辞書として使い続けたい場合は Object.create(null) でプロトタイプのないオブジェクトを作る方法もあります。

配列とSetの違い・使い分け

順番や重複に意味があるなら配列、「入っているかどうか」だけが大事ならSetです。

比べる点 配列 Set
重複 持てる 持てない(自動で1つに)
存在確認 includes(先頭から順に探す) has(件数が増えても速い)
番号で取り出す arr[0] できない([...set][0])
並べ替え・map・filter そのまま使える 配列にするか、イテレーターヘルパー
要素の削除 splice / filter(位置が必要) delete(value)
集合演算 filter + includes で書く union / intersection / difference
JSON化 そのまま {} になる。[...set] にする

速さを実測:Object vs Map、配列 vs Set

判断の材料として、Node.js 22で追加・検索・削除の時間を測りました。 2コアのコンテナで、同じ処理を5回測った中央値です。環境で数値は揺れるため、2回実行した結果を両方載せます。

// bench.mjs  (node bench.mjs)
const N = 1_000_000;
const keys = Array.from({ length: N }, (_, i) => "key" + i);
const median = (a) => a.sort((x, y) => x - y)[Math.floor(a.length / 2)];
function measure(fn) {
  const t = [];
  for (let r = 0; r < 5; r++) { const s = performance.now(); fn(); t.push(performance.now() - s); }
  return median(t).toFixed(1) + " ms";
}
let sink = 0;
console.log(`文字列キー ${N.toLocaleString()} 件`);
console.log("追加  Object      ", measure(() => { const o = {}; for (const k of keys) o[k] = 1; sink += o.key0; }));
console.log("追加  Map         ", measure(() => { const m = new Map(); for (const k of keys) m.set(k, 1); sink += m.size; }));
const o = {}; for (const k of keys) o[k] = 1;
const m = new Map(); for (const k of keys) m.set(k, 1);
console.log("検索  Object(in)  ", measure(() => { let c = 0; for (const k of keys) if (k in o) c++; sink += c; }));
console.log("検索  Map.has     ", measure(() => { let c = 0; for (const k of keys) if (m.has(k)) c++; sink += c; }));
console.log("削除  delete obj  ", measure(() => { const x = {}; for (let i = 0; i < 100_000; i++) x[keys[i]] = 1; for (let i = 0; i < 100_000; i++) delete x[keys[i]]; }), "(10万件)");
console.log("削除  Map.delete  ", measure(() => { const x = new Map(); for (let i = 0; i < 100_000; i++) x.set(keys[i], 1); for (let i = 0; i < 100_000; i++) x.delete(keys[i]); }), "(10万件)");
const arr = keys.slice(0, 10_000);
const set = new Set(arr);
const q = keys.slice(5_000, 15_000);
console.log("存在確認 Array.includes", measure(() => { let c = 0; for (const k of q) if (arr.includes(k)) c++; sink += c; }), "(1万件×1万回)");
console.log("存在確認 Set.has       ", measure(() => { let c = 0; for (const k of q) if (set.has(k)) c++; sink += c; }), "(1万件×1万回)");
const rec = { id: 1, name: "pen", price: 120 };
const recMap = new Map(Object.entries(rec));
console.log("固定キー読み取り Object", measure(() => { let s = 0; for (let i = 0; i < 10_000_000; i++) s += rec.price; sink += s; }), "(1000万回)");
console.log("固定キー読み取り Map   ", measure(() => { let s = 0; for (let i = 0; i < 10_000_000; i++) s += recMap.get("price"); sink += s; }), "(1000万回)");
計測(Node.js v22.22.2) 1回目 2回目
追加 100万件:Object 561.8 ms 550.2 ms
追加 100万件:Map 586.6 ms 742.0 ms
検索 100万回:Object(in) 227.1 ms 106.1 ms
検索 100万回:Map.has 473.0 ms 237.3 ms
追加→削除 10万件:Object(delete) 70.1 ms 44.9 ms
追加→削除 10万件:Map.delete 44.8 ms 39.9 ms
存在確認 1万件×1万回:Array.includes 221.3 ms 143.3 ms
存在確認 1万件×1万回:Set.has 0.3 ms 1.0 ms
固定キー読み取り 1000万回:Object 7.5〜19.7 ms
固定キー読み取り 1000万回:Map 54.7〜104.6 ms

読み取り方は次のとおりです。

  • 配列の includes をSetの has に変えると、この条件では100倍以上速くなりました。 配列は先頭から順に探すため、件数×回数で時間が増えます。何度も「入っているか」を調べるなら、最初に一度Setを作るのが効果的です。
  • ObjectとMapの差は、この計測では数倍以内で、処理によって逆転します。 文字列キーの検索や決まったキーの読み取りはObjectが速く、追加と削除をくり返す処理はMapが速い結果でした。
  • ObjectとMapは速さより「用途」で選ぶのがおすすめです。 表の差が問題になるのは数十万件以上を何度も回す場合で、多くの処理ではキーの型・順番・JSON化のしやすさのほうが効いてきます。

コピペで使えるレシピ集

よく使う5つの処理を、そのまま動く形でまとめました。

// recipes.mjs
// 1. 重複除去
const ids = [3, 1, 3, 2, 1];
console.log([...new Set(ids)]);
// オブジェクト配列をidで重複除去(後から来たもので上書き)
const rows = [{ id: 1, v: "a" }, { id: 2, v: "b" }, { id: 1, v: "c" }];
console.log([...new Map(rows.map((r) => [r.id, r])).values()]);

// 2. 出現回数カウント(上位2件)
const words = ["pen", "note", "pen", "tape", "pen", "note"];
const freq = new Map();
for (const w of words) freq.set(w, (freq.get(w) ?? 0) + 1);
console.log(freq);
console.log([...freq].sort((a, b) => b[1] - a[1]).slice(0, 2));

// 3. グループ化(Map.groupByを使わない書き方。getOrInsertの代わりにもなる)
const tasks = [{ t: "設計", who: "佐藤" }, { t: "実装", who: "鈴木" }, { t: "レビュー", who: "佐藤" }];
const byWho = new Map();
for (const task of tasks) {
  if (!byWho.has(task.who)) byWho.set(task.who, []);
  byWho.get(task.who).push(task.t);
}
console.log(byWho);

// 4. キャッシュ(メモ化)
const cache = new Map();
let calls = 0;
function slowSquare(n) {
  if (cache.has(n)) return cache.get(n);
  calls++;
  const r = n * n;
  cache.set(n, r);
  return r;
}
slowSquare(4); slowSquare(4); slowSquare(5);
console.log({ calls, size: cache.size });

// 4'. サイズ上限つきキャッシュ(LRU): Mapが追加順を保つことを利用
class LRU {
  constructor(max) { this.max = max; this.map = new Map(); }
  get(k) {
    if (!this.map.has(k)) return undefined;
    const v = this.map.get(k);
    this.map.delete(k); this.map.set(k, v);   // 末尾へ移動
    return v;
  }
  set(k, v) {
    this.map.delete(k); this.map.set(k, v);
    if (this.map.size > this.max) this.map.delete(this.map.keys().next().value); // 先頭=最も古い
  }
}
const lru = new LRU(2);
lru.set("a", 1); lru.set("b", 2); lru.get("a"); lru.set("c", 3);
console.log([...lru.map.keys()]);

// 5. 配列の差分(追加・削除・共通)
const before = ["pen", "note", "ruler"];
const after = ["note", "ruler", "tape"];
const B = new Set(before), A = new Set(after);
console.log({ added: [...A.difference(B)], removed: [...B.difference(A)], kept: [...A.intersection(B)] });
// Set新メソッドがない環境(Node 20など)での書き方
console.log({ added: after.filter((x) => !B.has(x)), removed: before.filter((x) => !A.has(x)) });

// おまけ: Mapの並べ替え・絞り込み・変換
const price = new Map([["pen", 120], ["note", 300], ["tape", 80]]);
console.log(new Map([...price].sort((a, b) => a[1] - b[1])));
console.log(new Map(price.entries().filter(([, p]) => p >= 100)));   // イテレーターヘルパー
console.log(price.keys().map((k) => k.toUpperCase()).toArray());

実行結果(Node.js v22.22.2):

[ 3, 1, 2 ]
[ { id: 1, v: 'c' }, { id: 2, v: 'b' } ]
Map(3) { 'pen' => 3, 'note' => 2, 'tape' => 1 }
[ [ 'pen', 3 ], [ 'note', 2 ] ]
Map(2) { '佐藤' => [ '設計', 'レビュー' ], '鈴木' => [ '実装' ] }
{ calls: 2, size: 2 }
[ 'a', 'c' ]
{ added: [ 'tape' ], removed: [ 'pen' ], kept: [ 'note', 'ruler' ] }
{ added: [ 'tape' ], removed: [ 'pen' ] }
Map(3) { 'tape' => 80, 'pen' => 120, 'note' => 300 }
Map(2) { 'pen' => 120, 'note' => 300 }
[ 'PEN', 'NOTE', 'TAPE' ]

レシピごとの要点です。

レシピ 使うもの 要点
重複除去 [...new Set(arr)] 最初に出た順で残る。オブジェクトはMapでキーを決める
出現回数 map.set(k, (map.get(k) ?? 0) + 1) 並べ替えは [...map].sort()
グループ化 Map.groupBy / has→set→get 新しい環境なら getOrInsert(k, []).push(v)
キャッシュ has → get、なければ計算して set 上限つきは追加順を使ったLRU
配列の差分 difference / intersection 旧環境は filter + Set.has

WeakMap・WeakSetの使い方

WeakMapは「キーのオブジェクトがほかで使われなくなったら、組ごと自動で消える」Mapです。 オブジェクト本体を書き換えずに、外付けのデータ(閲覧回数、計算結果のキャッシュ、DOM要素ごとの状態など)を結びつけるのに向いています。

比べる点 Map / Set WeakMap / WeakSet
キー(Setは値) 何でも オブジェクトと、登録されていないSymbolだけ
キーへの参照 強い(Mapが持つ限り消えない) 弱い(ほかに参照がなければ消える)
size・ループ・keys() あり なし(いつ消えるか決まらないため)
使えるメソッド 全部 set / get / has / delete(Setは add / has / delete)
// weak.mjs
const meta = new WeakMap();
let item = { name: "pen" };
meta.set(item, { viewed: 3 });
console.log(meta.get(item), meta.has(item));
try { meta.set("pen", 1); } catch (e) { console.log(e.constructor.name + ": " + e.message); }
console.log(meta.size, typeof meta.keys, typeof meta.forEach);
const ws = new WeakSet();
ws.add(item);
console.log(ws.has(item));
try { ws.add(1); } catch (e) { console.log(e.constructor.name + ": " + e.message); }
const sym = Symbol("k");
meta.set(sym, "symbol key");
console.log(meta.get(sym));
try { meta.set(Symbol.for("reg"), 1); } catch (e) { console.log(e.constructor.name + ": " + e.message); }

実行結果(Node.js v22.22.2):

{ viewed: 3 } true
TypeError: Invalid value used as weak map key
undefined undefined undefined
true
TypeError: Invalid value used in weak set
symbol key
TypeError: Invalid value used as weak map key

文字列や数値をキーにすると TypeError: Invalid value used as weak map key になります。 Symbol.for() で登録したSymbolもキーにできません(どこからでも取り出せるため、消える時期が決まらない)。

メモリが本当に解放されるかを実測

20万個のオブジェクトをキーにして登録し、キーへの参照を手放してからGC(ガベージコレクション=不要なメモリの回収)を実行しました。

// gc.mjs  (node --expose-gc gc.mjs で実行)
const mem = () => (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + " MB";
function run(Kind) {
  global.gc();
  const store = new Kind();
  let keys = [];
  for (let i = 0; i < 200_000; i++) { const k = { i }; keys.push(k); store.set(k, new Array(20).fill(i)); }
  const filled = mem();
  keys = null;                // キーへの参照を手放す
  global.gc();
  return `${Kind.name}: 登録直後 ${filled} → キーを手放してGC後 ${mem()}` + (store.size !== undefined ? ` (size=${store.size})` : "");
}
console.log(run(Map));
console.log(run(WeakMap));
Map: 登録直後 63.7 MB → キーを手放してGC後 56.3 MB (size=200000)
WeakMap: 登録直後 65.6 MB → キーを手放してGC後 11.7 MB

Mapはキーを持ち続けるので20万件が残り、WeakMapは組ごと回収されました。 「オブジェクトに付けたデータを、オブジェクトと一緒に片づけたい」場面ではWeakMapを選ぶと、delete の書き忘れを気にせずに済みます。

よくあるつまずきと直し方

MapとSetで迷いやすい点を、実行結果つきでまとめます。

// pitfalls.mjs
// 1) オブジェクト・配列のキーは「参照」で比較
const m = new Map();
m.set({ id: 1 }, "A");
console.log(m.get({ id: 1 }));          // 別のオブジェクトなので見つからない
const key = { id: 1 };
m.set(key, "B");
console.log(m.get(key));
m.set([1, 2], "arr");
console.log(m.has([1, 2]));
// 直し方: プリミティブのキーにする
const m2 = new Map();
const k = (p) => `${p.x},${p.y}`;
m2.set(k({ x: 1, y: 2 }), "点A");
console.log(m2.get(k({ x: 1, y: 2 })));
// 2) Setも参照で比較
const s = new Set([{ id: 1 }, { id: 1 }]);
console.log(s.size);
// 3) JSON.stringify は {} になる
const map = new Map([["pen", 10]]);
const set = new Set(["js", "css"]);
console.log(JSON.stringify(map), JSON.stringify(set));
console.log(JSON.stringify({ map, set }));
// 直し方: 配列/オブジェクトへ変換して保存、読み込み時に戻す
const json = JSON.stringify({ map: [...map], set: [...set] });
console.log(json);
const parsed = JSON.parse(json);
console.log(new Map(parsed.map), new Set(parsed.set));
console.log(JSON.stringify(Object.fromEntries(map)));
// replacer でまとめて変換
const replacer = (_k, v) => v instanceof Map ? Object.fromEntries(v) : v instanceof Set ? [...v] : v;
console.log(JSON.stringify({ map, set }, replacer));
// 4) ブラケットで代入してもMapの中身にはならない
const wrong = new Map();
wrong["pen"] = 10;
console.log(wrong.get("pen"), wrong.size, wrong.pen);
// 5) NaN をキーにできる、0と-0は同じ
const z = new Map([[NaN, "nan"], [0, "zero"]]);
console.log(z.get(NaN), z.get(-0));
// 6) ループ中に追加した値もそのループで回る
const loop = new Set([1]);
let count = 0;
for (const v of loop) { if (v < 3) loop.add(v + 1); count++; }
console.log([...loop], count);
// 7) structuredClone はMap/Setを複製できる
const c = structuredClone(map);
console.log(c, c !== map);

実行結果(Node.js v22.22.2):

undefined
B
false
点A
2
{} {}
{"map":{},"set":{}}
{"map":[["pen",10]],"set":["js","css"]}
Map(1) { 'pen' => 10 } Set(2) { 'js', 'css' }
{"pen":10}
{"map":{"pen":10},"set":["js","css"]}
undefined 0 10
nan zero
[ 1, 2, 3 ] 3
Map(1) { 'pen' => 10 } true
つまずき 起きること 直し方
オブジェクトをキーにして別のオブジェクトで get undefined(中身が同じでも別物) 同じ参照を使う/"x,y" や id などのプリミティブをキーにする
同じ中身のオブジェクトをSetに入れる 重複が消えない(size が2) new Map(arr.map(o => [o.id, o])) でキーを決める
JSON.stringify(map) {} になる [...map] か Object.fromEntries(map)、読み込みは new Map(...)
map["pen"] = 10 と書く Mapの中身ではなく普通のプロパティになる(size は0) map.set("pen", 10)
map.size() と書く TypeError(size は関数ではない) map.size
forEach((key, value) => …) キーと値が逆になる forEach((value, key) => …)
Setの新メソッドに配列を渡す TypeError: The .size property is NaN new Set(arr) で包む
ループ中に add 追加した値もそのループで回る 先に [...set] で配列にしてから回す

深いコピーが必要なら structuredClone(map) が使えます。 MapとSetは中身ごと複製されます(キーのオブジェクトも複製されるので、元のオブジェクトでは get できなくなる点に注意します)。

TypeScriptで型をつける

TypeScriptでは Map<キーの型, 値の型>・Set<値の型> と書きます。 空で作るときは型引数を書いておくと、あとで入れる値がチェックされます。

const stock = new Map<string, number>();
stock.set("pen", 10);
const qty = stock.get("pen");          // number | undefined
const safe = stock.get("pen") ?? 0;    // number
const tags = new Set<string>(["js"]);
const byStatus: Map<"shipped" | "pending", { id: number }[]> = new Map();

get の戻り値は常に「値の型 | undefined」になります。 has で確かめたあとでも undefined が外れないため、?? で既定値を入れるか、上のレシピのように get の結果を変数に受けてから確かめます。

よくある質問

Q. MapとObject、結局どちらを使えばよいですか? A. 項目名が決まっているデータはObject、キーがあとから増減する辞書やキャッシュはMapがおすすめです。キーが文字列以外、追加順を保ちたい、件数を頻繁に数える、外部入力の文字列をキーにする、のいずれかに当てはまればMapを選びます。JSONでそのまま送りたいデータはObjectのほうが扱いやすいです。

Q. Setの union や intersection はいつから使えますか? A. ES2025の機能で、Node.js 22以降と主要ブラウザの現行版で使えます。 この記事ではNode.js 22.22.2とChromium 141で動作を確認しました。Node 20を対象にする場合は、レシピの filter + has の書き方を使います。

Q. MapやSetの中身をlocalStorageに保存するには? A. JSON.stringify([...map]) で配列にしてから保存し、読み込むときに new Map(JSON.parse(text)) で戻します。 そのまま JSON.stringify(map) すると {} になります。Setも [...set] と new Set(...) で同じように扱えます。

Q. WeakMapはどんなときに使いますか? A. オブジェクトに付けたデータを、そのオブジェクトと一緒に自動で片づけたいときです。 たとえばDOM要素ごとの状態や、オブジェクトごとの計算結果のキャッシュです。中身を一覧したい・件数を知りたい場合は、通常のMapを使います。

まとめ

  • Mapは何でもキーにできる順番つき辞書、Setは重複しない値の集まり。件数は size(プロパティ)
  • Setの新メソッド7つ(union・intersection・difference など)はNode 22で使える。引数は配列ではなくSet
  • グループ分けは Map.groupBy、キーにオブジェクトを使えるのが Object.groupBy との違い
  • 決まった項目のデータはObject、増減する辞書・キャッシュ・外部入力のキーはMap
  • 存在確認をくり返すなら配列よりSet。実測では includes から has で100倍以上速くなった
  • オブジェクトのキーは参照で比較、JSON.stringify は {}。配列やプリミティブに変換して扱う
  • オブジェクトに外付けするデータはWeakMap。キーを手放すと組ごと回収される

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript Map・Set|使い方とObject・配列との違い一覧」

株式会社RJCでは、JavaScriptのMapとSetの使い分けを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?