- JavaScript
JavaScript Map・Set|使い方とObject・配列との違い一覧
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期連続の黒字決算。