Q1const ids = new Set(["u-1", "u-2"]); のあとに ids.add("u-1"); を実行すると、ids.size はいくつですか?
Map と Set — 重複排除とキー付き集計
同じ値を 1 つしか持たない Set と、キーと値の組を順番どおりに持つ Map を扱います。重複の取り除き方、追加と削除、キーで値を引く書き方、Object との使い分けまで進みます。
商品に付いたタグを配列で集めると同じタグが何度も混ざり、実際に何種類あるかを知るには includes で毎回確かめる手間が要ります。商品 ID ごとの在庫数のようにキーと値の組を持ちたいときも、配列だけでは ID と数量を 2 本の配列に分けて対応づける必要があり、オブジェクトのキーは文字列と symbol に限られるため数値をそのままキーにはできません。
この記事では、重複を持たない Set と、文字列以外もキーにできる Map を扱います。
重複を取り除く — Set の作り方
タグ付きの商品一覧から、実際に使われているタグが何種類あるかを知りたいとします。配列のままでは同じタグが複数回並ぶため、そのまま数えると件数が水増しされます。重複をあらかじめ取り除いて持てるデータ構造が必要です。
Set(同じ値を 1 つしか持たないデータ構造)は new Set(tags) のように配列を渡して作ります。new を先頭に付けると、Set や Map のような決まった種類のデータ構造を新しく 1 つ作るという意味になります。
同じ値が来ても増えないので、size を読むだけで種類の数が分かります。同じ値かどうかは === と同じ基準で判定されます。
Set は配列ではないため、uniqueTags[0] のように index を指定しても読み出せません。配列として扱いたいときは [...uniqueTags] のようにスプレッド構文で変換します。
並び順は追加された順のまま保たれるので、元の配列に現れた順序がそのまま残ります。
const tags = ["セール", "新着", "セール", "送料無料"];
// 配列を渡すと、重複を取り除いた Set ができる
const uniqueTags = new Set(tags);
console.log(uniqueTags.size); // 3
// スプレッド構文で配列に戻す(追加された順のまま)
console.log([...uniqueTags].join(", ")); // セール, 新着, 送料無料
// 元の配列はそのまま
console.log(tags.length); // 4
// 空の Set から作り始めることもできる
const empty = new Set();
console.log(empty.size); // 0
値を追加・削除する — add と has と delete
訪問したユーザー ID を記録しておき、同じ人が何度来ても 1 人として数えたい場面があります。配列で持つと、追加のたびに includes で調べてから push する必要があり、記録件数が多いほど確認に時間がかかります。
Set には値を足す add、持っているかを調べる has、取り除く delete、件数を読む size があります。add は同じ値が来ても何も起こらないので、追加前に確認する必要がありません。
has は true か false を返し、delete は取り除けたかどうかを返します。
add しても件数は増えません。取り除くときだけ件数が減ります。const visitors = new Set(["u-102", "u-337"]);
// 持っているかを調べる
console.log(visitors.has("u-102")); // true
console.log(visitors.has("u-999")); // false
// 足す。同じ値なら件数は変わらない
visitors.add("u-541");
console.log(visitors.size); // 3
visitors.add("u-541");
console.log(visitors.size); // 3
// 取り除く。取り除けたかどうかが返る
console.log(visitors.delete("u-102")); // true
console.log(visitors.delete("u-102")); // false(もう無い)
console.log(visitors.size); // 2
キーと値の組を持つ — Map
商品 ID ごとの在庫数のように、キーと値を組にして持ちたい場面があります。オブジェクトでも実現できますが、ID のように実行時に決まるキーを集計したり、あとから増減させたりする用途には、専用のメソッドを持つデータ構造のほうが向いています。
Map(キーと値の組を登録した順に持つデータ構造)は new Map() で作り、set(キー, 値) で登録します。読むときは get(キー)、あるかどうかは has(キー)、件数は size です。
同じキーで set をやり直すと値が上書きされます。最初から中身を入れるときは new Map([["P-001", 12], ["P-002", 5]]) のように、組の配列を渡します。
set で組を登録し、get でキーから値を引きます。登録が無いキーは undefined が返ります。const stock = new Map();
// set で組を登録する
stock.set("P-001", 12);
stock.set("P-002", 5);
console.log(stock.size); // 2
// get でキーから値を引く
console.log(stock.get("P-002")); // 5
console.log(stock.get("P-999")); // undefined
console.log(stock.has("P-999")); // false
// 同じキーで set し直すと上書きされる
stock.set("P-001", 8);
console.log(stock.get("P-001")); // 8
console.log(stock.size); // 2(件数は増えない)
// 最初から組を入れて作ることもできる
const prices = new Map([["P-001", 3980], ["P-002", 2480]]);
console.log(prices.get("P-001")); // 3980
Map の値はドット記法では読めない
stock.get("P-001") のかわりに stock["P-001"] と書いても値は返りません。この書き方は Map 自身のプロパティを探しにいくため、登録した組とは無関係で undefined が返ります。読み書きは get と set を通します。
Object との使い分け — キーの型と並び順
キーと値を持つという点ではオブジェクトと同じなので、どちらで書くかを選びます。設定項目のようにキーがコードを書く時点で決まっているならオブジェクト、ID や日付のように実行時に増えていくキーを集計するなら Map が扱いやすくなります。
判断の材料になるのが、キーの型と並び順です。
オブジェクトのキーは文字列に変換されるので、record[2026] と書いても実際のキーは "2026" です。Map は渡した値をそのままキーにするため、数値の 2026 と文字列の "2026" は別のキーとして扱われます。
並び順も、Map は登録した順をそのまま保ちます。
2026 と文字列の "2026" を 別のキーとして区別 します。オブジェクトは文字列に変換します。| 観点 | Object | Map |
|---|---|---|
| キーに使える型 | 文字列に変換される | どの型でもそのまま |
| 並び順 | 数字のキーが先に並ぶ | 登録した順のまま |
| 件数の数え方 | Object.keys の length | size |
| 値の読み書き | obj.key / obj[key] | get と set |
| 向いている場面 | 決まった項目の集まり | 実行時に増えるキーの集計 |
Map の中身を一覧するときは [...stock.entries()] と書いて、[キー, 値] の 2 要素の配列が並んだ配列に直します。こうすると entries[0] のように 1 組ずつ取り出せます。組の数ぶん同じ処理を繰り返す書き方は、繰り返しを学ぶ記事であらためて扱います。
キーだけが必要なときは stock.keys() を使いますが、これが返すのは配列ではないので join を直接つなげられません。[...stock.keys()] と書いて配列にしてから使います。
const scores = new Map([["math", 80]]);
// Map は数値のキーを数値のまま持つ
scores.set(2026, "今年の集計");
console.log(scores.get(2026)); // 今年の集計
console.log(scores.get("2026")); // undefined(別のキー)
// オブジェクトはキーが文字列に変換される
const record = {};
record[2026] = "今年の集計";
console.log(Object.keys(record)[0]); // 2026
console.log(typeof Object.keys(record)[0]); // string
console.log(record["2026"]); // 今年の集計(同じキー扱い)
// keys() / entries() はスプレッド構文で配列にしてから使う
console.log([...scores.keys()].join(", ")); // math, 2026
const pairs = [...scores.entries()];
console.log(pairs[0].join(": ")); // math: 80
// オブジェクトは整数のようなキーが先に並ぶ
const ranking = {};
ranking[3] = "c";
ranking[1] = "a";
console.log(Object.keys(ranking).join(", ")); // 1, 3
理解度チェック
まずは1問ずつ答えてみましょう。
Q2stock が Map のとき、登録していないキーを stock.get("P-999") で引くと何が返りますか?
Q3数値の 2026 をキーとして登録したとき、Map とオブジェクトで違うのはどこですか?