重複削除とは何か
重複削除は「同じ値や同じレコードが複数含まれる配列から、重複を取り除き“1回だけ”にする」処理です。ここが重要です:何を“同じ”とみなすかで手法が変わります。プリミティブ(数値・文字列)は値一致、オブジェクトは“IDやキー”で一致を判定します。結果の順序(先に出たものを残すか、最後を残すか)を意識することも実務では重要です。
プリミティブの重複削除(Set が最短)
Set で一意化(先に出た順を維持)
const nums = [1, 2, 2, 3, 1];
const unique = [...new Set(nums)]; // [1, 2, 3]
JavaScriptSet は同じ値を1つにまとめ、最初に出現した順を保ちます。ここが重要です:NaN は同値として扱われ、-0 と 0 は同一視されます。これで困るケースは少ないですが、意味の違いが重要なら別の判定方法を選びます。
filter+indexOf の古典的書き方(理解用)
const unique = nums.filter((v, i, arr) => arr.indexOf(v) === i);
JavaScript動作は分かりやすいものの、一般に Set より遅くなりがちです。実務では Set を優先するのが無難です。
オブジェクト配列の重複削除(IDで重複を落とす)
先勝ち(最初に出たレコードを残す)
const users = [
{ id: 1, name: "Alice" },
{ id: 1, name: "Alice v2" },
{ id: 2, name: "Bob" }
];
const seen = new Set();
const unique = users.filter(u => {
if (seen.has(u.id)) return false;
seen.add(u.id);
return true;
});
// [{ id:1, name:"Alice" }, { id:2, name:"Bob" }]
JavaScriptここが重要です:先勝ちは“履歴の最初を尊重”したい場面に適します。ログの初出採用や、重複投入の早期レコードを優先したいときに向きます。
後勝ち(最後に出たレコードを残す)
const byId = new Map(users.map(u => [u.id, u]));
const unique = [...byId.values()];
// [{ id:1, name:"Alice v2" }, { id:2, name:"Bob" }]
JavaScript同じ ID が後から来たら上書きされるため、最終状態を採用できます。設定や最終更新を反映したいときはこちらが適します。
任意キーでの重複削除(uniqueBy ユーティリティ)
キー関数で柔軟に“同値”を定義する
function uniqueBy(list, keyFn, keep = "first") {
if (keep === "last") {
const m = new Map(list.map(x => [keyFn(x), x]));
return [...m.values()];
}
const seen = new Set();
const out = [];
for (const x of list) {
const k = keyFn(x);
if (!seen.has(k)) {
seen.add(k);
out.push(x);
}
}
return out;
}
// 使い方
const products = [
{ sku: "A-001", name: "Pen" },
{ sku: "a-001", name: "Pen v2" },
{ sku: "B-010", name: "Book" }
];
const normalizeSku = s => s.trim().toLowerCase();
const uniqueSkuFirst = uniqueBy(products, p => normalizeSku(p.sku), "first");
const uniqueSkuLast = uniqueBy(products, p => normalizeSku(p.sku), "last");
JavaScriptここが重要です:キー関数で“表記ゆれ”を吸収(小文字化・トリム・全角半角変換)すれば、現実のデータに強い重複削除になります。仕様として“何を同じとみなすか”を明確化し、それを keyFn に反映します。
ネスト・グループ跨ぎの重複削除(平坦化して処理)
複数グループを横断して一意化
const groups = [
[{ id: 1, n: "A" }, { id: 2, n: "B" }],
[{ id: 2, n: "B v2" }, { id: 3, n: "C" }]
];
const flat = groups.flat();
const uniqueFirst = uniqueBy(flat, x => x.id, "first");
// 先勝ちの結果を得る
JavaScriptここが重要です:ネスト配列はまず flat() で平坦化してから重複削除すると、意図が明確で保守しやすくなります。後勝ちにしたい場合は Map 方式や uniqueBy の “last” を使います。
高度な同値判定(構造キー・深い比較を使う場合)
構造を文字列キーにして重複削除(簡易)
const items = [{a:1,b:2}, {b:2,a:1}, {a:1,b:3}];
const canon = obj => JSON.stringify(Object.fromEntries(
Object.entries(obj).sort(([ka],[kb]) => ka.localeCompare(kb))
));
const unique = [...new Map(items.map(x => [canon(x), x])).values()];
// {a:1,b:2} と {a:1,b:3} の一意化
JavaScriptここが重要です:深い比較はコストが高いので、可能なら“IDや正規化キー”を用意する設計が望ましいです。プロパティ順差で別物にならないよう、キーをソートしてから文字列化します。
実務での安全設計(重要ポイントの深掘り)
同値の定義を先に決める
何を同じとみなすか(ID、大小無視、空白・全角半角、数値スケール)を仕様として明文化します。ここが曖昧だと結果が不安定になります。
先勝ち・後勝ちの選択を明示する
入力順で結果が変わるため、要件に合わせて「first か last か」を選び、コードに明示します。設定や上書きは後勝ち、履歴や初出は先勝ちが目安です.
参照の安全性(イミュータブルを徹底)
重複削除後の更新は“非破壊更新”で行います。スプレッドや map で新レコードを作り、辞書化(Object.fromEntries / Map)を併用すると部分更新が簡潔で安全です。
性能とメモリのバランス
Set/Map のサイズは重複が多いほど大きくなります。巨大データでは処理を分割する、ストリーム的に区間ごとに重複削除するなど、スコープ設計でメモリを抑えます。
すぐ使えるレシピ(短く安全な重複削除)
プリミティブの重複削除(先勝ち)
const dedupe = arr => [...new Set(arr)];
JavaScriptオブジェクトの重複削除(ID・先勝ち/後勝ち)
const dedupeByIdFirst = arr => {
const seen = new Set();
return arr.filter(x => !seen.has(x.id) && (seen.add(x.id), true));
};
const dedupeByIdLast = arr => [...new Map(arr.map(x => [x.id, x])).values()];
JavaScript任意キーで重複削除(表記ゆれ対応)
const dedupeBy = (arr, keyFn, keep = "first") => uniqueBy(arr, keyFn, keep);
// keyFn 例:s => s.trim().toLowerCase()
JavaScriptまとめ
重複削除は「Set/Map を使って、定義した“同値”に基づいて一意化する」ことが核心です。プリミティブは Set、オブジェクトは ID やキー関数で判定し、要件に応じて先勝ち・後勝ちを選ぶ。ネストは平坦化してから処理し、深い比較はコストが高いので“比較キーの設計”を優先する。イミュータブルを徹底し、性能はデータ規模に合わせて分割・辞書化で最適化する。これを押さえれば、初心者でも短く、正確で実務に耐える重複削除が書けます。
