【JavaScript】配列の重複を削除する方法|Set・filter・Mapでオブジェクト配列・NaNの罠まで解説

【JavaScript】配列の重複を簡単に削除する方法(Set・filterの活用) JavaScript

配列から重複した値を取り除く処理は、JavaScriptでは数行で書けます。ただし「プリミティブかオブジェクトか」「最初と最後どちらを残すか」「NaN を含むか」によって最適な方法は変わります。この記事では代表的な手法を、計算量や落とし穴も含めて使い分けられるように整理します。

この記事でわかること

  • 最速・最短の [...new Set(array)] による重複削除
  • filter + indexOf 方式と、その計算量(O(n²))
  • NaN が消える落とし穴(手法で挙動が変わる)
  • オブジェクト配列をキー基準で重複削除する(Map / findIndex
  • 最初と最後、どちらの要素を残すかの制御
  • 複合キー・参照型の注意点と、手法の使い分け早見表
結論:プリミティブ配列は [...new Set(array)] が最速・最短で NaN もまとめられます。オブジェクト配列はキーごとに Map(最後が残る・O(n))か findIndex(最初が残る・O(n²))。大きい配列なら Set を使った O(n) の方法を選びます。
スポンサーリンク

基本:Set を使う(最速・最短)

ES6 の Set は同じ値を持てない集合です。配列を Set に通すと重複が自動的に排除され、スプレッド構文で配列へ戻せます。挿入順が保持されるため、元の並びも崩れません。

JavaScript:Set で重複削除
const array = [1, 2, 2, 3, 4, 4, 5];

const unique = [...new Set(array)];
console.log(unique); // [1, 2, 3, 4, 5]

// Array.from でも同じ(map 関数を第2引数に渡せるのが利点)
const unique2 = Array.from(new Set(array));

文字列の配列でもまったく同じように使えます。プリミティブ(数値・文字列・真偽値)の配列なら、まずこの方法を選んでおけば間違いありません。

filter + indexOf を使う(条件を絡めたいとき)

filterindexOf の組み合わせは、「その値が最初に現れる位置」と「現在のインデックス」が一致する要素だけを残す方法です。重複判定に独自の条件を絡めたいときに使えます。

JavaScript:filter + indexOf
const array = [1, 2, 2, 3, 4, 4, 5];

const unique = array.filter((value, index, self) => {
  return self.indexOf(value) === index; // 最初に現れた位置だけ残す
});
console.log(unique); // [1, 2, 3, 4, 5]
計算量に注意:filter + indexOf は O(n²)
indexOf は要素ごとに配列を先頭から探索するため、filter と組み合わせると要素数の2乗に比例して遅くなります。数十件なら問題ありませんが、数千〜数万件の配列では Set 方式(O(n))が圧倒的に高速です。

【重要】NaN を含む配列の落とし穴

配列に NaN が含まれると、手法によって結果が変わります。これは見落としやすい罠です。

JavaScript:NaN で挙動が分かれる
const data = [1, NaN, 2, NaN, 2];

// Set は NaN を「1つ」にまとめられる
console.log([...new Set(data)]);                          // [1, NaN, 2]

// filter + indexOf は NaN が「すべて消える」
console.log(data.filter((v, i, s) => s.indexOf(v) === i)); // [1, 2] ← NaN が消失!
なぜ消える?
indexOf=== で比較しますが、NaN === NaNfalse です。そのため indexOf(NaN) は常に -1 を返し、-1 === index が成立せずどの NaN も残りません。一方 Set は SameValueZero という比較を使い NaN 同士を等しいと扱うため、1つだけ残せます。NaN が混じる可能性があるなら Set を使ってください。

オブジェクト配列をキー基準で重複削除する

オブジェクトは中身が同じでも参照が違えば別物として扱われるため、Set をそのまま使っても重複は消えません(後述)。id など特定のキーを基準に重複を判定します。Map を使うと O(n) で書けて高速です。

JavaScript:Map でキー基準の重複削除(最後が残る)
const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 1, name: "Carol" },
];

// id をキーに Map へ詰める → 同じ id は上書きされる
const unique = [...new Map(users.map((u) => [u.id, u])).values()];
console.log(unique);
// [{ id: 1, name: "Carol" }, { id: 2, name: "Bob" }]  ← 同じ id は「最後」が残る

findIndex を使う方法もあります。こちらは「最初」の要素を残しますが、findIndex が要素ごとに探索するため O(n²) です。

JavaScript:findIndex で重複削除(最初が残る・O(n²))
const unique = users.filter((u, i, self) =>
  i === self.findIndex((x) => x.id === u.id) // 最初の出現だけ残す
);
// [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]  ← 同じ id は「最初」が残る

最初を残す?最後を残す?(順序を保ったまま O(n))

「最初の出現を残しつつ、O(n) で高速に処理したい」場合は、確認済みのキーを Set に記録しながら filter する方法が最適です。プリミティブにもオブジェクトにも使え、元の順序も保たれます。

JavaScript:seen Set で「最初」を残す(O(n)・順序保持)
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" }]
最初/最後の使い分け
最初を残すseen Set または findIndex最後を残すMap(同じキーで上書きされるため)。「最後を残しつつ O(n)」も Map ならそのまま実現できます。

複数プロパティ(複合キー)で重複削除する

typename の組み合わせが同じなら重複」のように、複数の値で判定したいときは、キーを連結して1つの文字列にします。

JavaScript:複合キーで重複削除
const seen = new Set();
const unique = items.filter((x) => {
  const key = `${x.type}|${x.name}`; // 値に含まれない区切り文字で連結
  if (seen.has(key)) return false;
  seen.add(key);
  return true;
});
区切り文字の選び方
連結する値の中に区切り文字(上の例では |)が含まれると、"a|b" + "c""a" + "b|c" が同じキーになってしまう誤判定が起きます。値に現れない文字を選ぶか、確実を期すなら JSON.stringify([x.type, x.name]) をキーにします。

参照型の注意:中身が同じでも別物

Set=== はオブジェクトを参照(同一インスタンスか)で比較します。中身がまったく同じでも、別々に作られたオブジェクトは重複とみなされません。

JavaScript:参照が違えば重複扱いされない
const arr = [{ a: 1 }, { a: 1 }];

console.log([...new Set(arr)].length); // 2 ← 中身は同じでも参照が違うので残る

中身で重複判定したい場合は、前述のように特定キーJSON.stringify で「値の同一性」を表すキーを作って判定します。オブジェクトの等価性については配列を比較する方法(=== が使えない理由・JSON.stringify)、コピーの仕組みはオブジェクト・配列をディープコピーする方法も参考になります。

手法の使い分け早見表

手法 対象 計算量 残るのは NaN
[...new Set()] プリミティブ O(n) 最初 1つ残る
filter + indexOf プリミティブ O(n²) 最初 全消失
Map(キー基準) オブジェクト O(n) 最後
findIndex オブジェクト O(n²) 最初
seen Set 両方 O(n) 最初 キー次第

配列操作全般は配列の使い方まとめ(破壊的/非破壊的メソッド)、条件抽出や集計は配列から条件に合う要素を取り出す方法(filter・find)配列の map・filter・reduce の使い方、存在チェックはincludes の使い方indexOf の使い方も合わせてご覧ください。

よくある質問(FAQ)

QSet と filter + indexOf はどちらを使うべきですか?
A基本は [...new Set(array)] を選びます。O(n) で速く、NaN もまとめられ、コードも最短です。filter + indexOf は重複判定に独自条件を絡めたいときや、学習目的で仕組みを示したいときに使う程度で十分です。
QNaN や undefined を含む配列はどう扱えばいいですか?
Aundefined はどの方法でも問題ありませんが、NaNfilter + indexOf だとすべて消えてしまいますNaN === NaN が false のため)。NaN が混じる可能性があるなら Set を使ってください。
Qオブジェクト配列で最初と最後、どちらの要素を残せますか?
AMap にキーで詰める方法は最後の要素が残ります(同じキーで上書きされるため)。最初を残したいなら、確認済みキーを Set に記録しながら filter する方法か、findIndex を使います。
Q中身が同じオブジェクトを重複として削除したいです。
ASet=== は参照で比較するため、別インスタンスは重複になりません。id などの特定キー、または JSON.stringify(obj) を重複判定のキーにして処理します。等価性の考え方は配列を比較する方法を参照してください。
Q重複削除の前後で配列の順序は保たれますか?
ASetMap はどちらも挿入順を保持するため、[...new Set(array)] や seen Set 方式は元の順序を保ったまま重複を削除できます。並び替えたい場合は重複削除のあとに sort() を適用してください。

まとめ

  • プリミティブ配列[...new Set(array)] が最速・最短(NaN も1つにまとまる)
  • filter + indexOf:O(n²) で大きい配列は遅い/NaN が消えるので注意
  • オブジェクト配列Map(最後・O(n))か findIndex(最初・O(n²))
  • 最初を残して高速に:seen Setfilter(O(n)・順序保持)
  • 複合キー:値を連結 or JSON.stringify をキーにする
  • 参照型:中身が同じでも参照が違えば別物 → キー化して判定

配列の規模・要素の型・残したい要素に応じて、適切な手法を選んでください。