はい、承知いたしました。JavaScriptの配列から重複を削除する方法に関する詳細な解説記事を作成します。約5000語という指定ですが、技術記事でこの文字数を達成するには、各項目を網羅的かつ非常に詳細に、理論や応用例、注意点などを深掘りする必要があります。冗長にならないよう注意深く記述しますが、一般的な技術ブログ記事としては異例な長さになることをご了承ください。
それでは、記事の内容を直接表示します。
JavaScript 配列 重複 削除:初心者でもわかる完全ガイド
JavaScriptでのプログラミングにおいて、配列は最も基本的なデータ構造の一つです。様々なデータをリスト形式で保持できるため、Webアプリケーション開発では欠かせません。しかし、データを扱っていると、意図せず配列の中に同じ値(重複)が含まれてしまうことがあります。
例えば、ユーザーが入力したタグのリスト、APIから取得した商品IDのリスト、ログデータなど、重複した情報が含まれていると、正確な集計や処理の妨げになったり、ユーザーインターフェース上で混乱を招いたりする可能性があります。
この記事では、JavaScriptの配列から重複した要素を取り除き、ユニークな(一意な)要素だけの配列を作成するための様々な方法を、初心者の方にも分かりやすく徹底的に解説します。なぜその方法が動くのか、それぞれのメリット・デメリット、そして実際のコード例を豊富に紹介します。
さあ、配列の重複削除をマスターして、よりクリーンで正確なデータ処理を目指しましょう!
1. なぜ配列の重複を削除する必要があるのか?
プログラミングの世界では、データの「正確性」と「効率性」が非常に重要です。配列に重複が含まれていると、以下のような問題が発生する可能性があります。
- 集計や分析の誤り: 例えば、ユーザーが選んだ商品の数を集計する際に、誤って同じ商品を複数カウントしてしまう。
- 表示上の混乱: Webサイト上で同じ項目が何度も表示され、ユーザー体験が悪化する。
- 処理の非効率化: 重複したデータに対して無駄な処理を実行してしまう。
- データベースへの無駄な書き込み: 重複したデータをそのままデータベースに保存してしまう。
これらの問題を避けるためには、配列から重複を削除し、ユニークな値だけを扱う必要があります。
2. 重複削除の基本的な考え方
重複を削除すると一口に言っても、いくつかの考え方があります。
- 「重複」の定義: 何をもって「重複」と判断するか?JavaScriptでは、プリミティブ型(数値、文字列、真偽値、null, undefined, Symbol, BigInt)の場合は「値」が同じであれば重複とみなされます。しかし、オブジェクト型(
{}
や[]
)の場合は、「参照」が同じでない限り、たとえオブジェクトの中身(プロパティとその値)が全く同じでも、異なるオブジェクトとして扱われます。この違いは、重複削除の方法を選ぶ上で非常に重要です。 - 出力形式: 重複を削除した結果をどのように得るか?
- 元の配列自体を変更する(破壊的変更)。
- 重複を削除した新しい配列を作成する(非破壊的変更)。
JavaScriptでは、非破壊的な方法(新しい配列を作成する)が一般的で、元のデータを保持できるため安全性が高いとされています。この記事で紹介する多くの方法も、新しい配列を作成するものです。
これから、これらの基本的な考え方を踏まえつつ、具体的な重複削除の方法を見ていきましょう。様々な方法がありますが、それぞれに得意な状況やパフォーマンスの違いがあります。
3. 方法1:Setオブジェクトを使う (最もシンプルかつ効率的)
JavaScriptのSet
オブジェクトは、ES6で導入された新しいデータ構造です。Set
の最大の特徴は、重複しない値のコレクションであるということです。この特性を利用すれば、配列の重複削除が非常に簡単に行えます。
3.1. Setオブジェクトとは?
Set
は、コレクション内の各値が一意である必要がある場合に役立ちます。つまり、同じ値を複数回add()
しようとしても、セットには一度しか追加されません。
3.2. Setを使った重複削除の原理
- 元の配列から
Set
オブジェクトを作成します。Set
を作成する際に、配列を渡すと、その配列に含まれる要素がSet
に追加されます。このとき、Set
の特性により、重複する要素は自動的に排除されます。 Set
オブジェクトは配列ではありません。もし結果を配列として得たい場合は、作成したSet
オブジェクトから改めて配列を作成し直す必要があります。これは、Array.from()
メソッドやスプレッド構文(...
)を使って簡単に行えます。
3.3. 具体的なコード例
最もシンプルで一般的な方法です。
“`javascript
// 重複を含む配列
const numbers = [1, 2, 3, 4, 2, 1, 5, 6, 3, 7];
const fruits = [“apple”, “banana”, “orange”, “apple”, “mango”, “banana”];
// Setを使って重複を削除し、新しい配列を作成する方法
// 方法A: Array.from()を使う
const uniqueNumbersA = Array.from(new Set(numbers));
const uniqueFruitsA = Array.from(new Set(fruits));
console.log(“— Set + Array.from() —“);
console.log(numbers); // 元の配列は変更されない: [1, 2, 3, 4, 2, 1, 5, 6, 3, 7]
console.log(uniqueNumbersA); // 重複削除後の新しい配列: [1, 2, 3, 4, 5, 6, 7]
console.log(fruits); // 元の配列は変更されない: [“apple”, “banana”, “orange”, “apple”, “mango”, “banana”]
console.log(uniqueFruitsA); // 重複削除後の新しい配列: [“apple”, “banana”, “orange”, “mango”]
// 方法B: スプレッド構文 (…) を使う
const uniqueNumbersB = […new Set(numbers)];
const uniqueFruitsB = […new Set(fruits)];
console.log(“\n— Set + スプレッド構文 —“);
console.log(numbers); // 元の配列は変更されない: [1, 2, 3, 4, 2, 1, 5, 6, 3, 7]
console.log(uniqueNumbersB); // 重複削除後の新しい配列: [1, 2, 3, 4, 5, 6, 7]
console.log(fruits); // 元の配列は変更されない: [“apple”, “banana”, “orange”, “apple”, “mango”, “banana”]
console.log(uniqueFruitsB); // 重複削除後の新しい配列: [“apple”, “banana”, “orange”, “mango”]
// 一行で書くことも多い
const uniqueNumbersOneLine = […new Set([1, 2, 3, 4, 2, 1, 5, 6, 3, 7])];
console.log(“\n— Set + スプレッド構文 (一行) —“);
console.log(uniqueNumbersOneLine); // [1, 2, 3, 4, 5, 6, 7]
“`
どちらの方法(Array.from()
またはスプレッド構文)を使っても結果は同じです。スプレッド構文の方がより短く書けるため、よく利用されます。
3.4. Setを使った重複削除のPros (メリット)
- シンプルさ: コードが非常に短く、意図が明確です。
- パフォーマンス: ほとんどの場合、他のどの方法よりも高速です。特に配列のサイズが大きい場合にその差は顕著になります。Setは内部的に効率的なデータ構造(ハッシュテーブルに似たもの)を使用しているため、要素の追加や存在チェックが高速に行えるからです。計算量はO(n)となります。
- 可読性:
new Set()
と書けば「重複をなくしたいのだな」とすぐに理解できます。
3.5. Setを使った重複削除のCons (デメリット)
- 環境依存性:
Set
オブジェクトはECMAScript 2015 (ES6) で導入されました。古いJavaScript環境(Internet Explorer 11以前など)ではそのままでは動作しません。ただし、Babelなどのトランスパイラを使えば、古い環境でも利用可能にすることができます。 - オブジェクトの重複判定: ここがSetを使う上での重要な注意点です。
Set
は、プリミティブ型の場合は値で重複を判断しますが、オブジェクト型の場合は「参照」が完全に一致しない限り、内容が全く同じでも異なるオブジェクトとして扱います。
3.6. Setによるオブジェクトの重複判定について詳しく
Setは、要素の等価性を判断する際に、厳密等価演算子(===
)に近いロジックを使用します。
- プリミティブ型:
5 === 5
はtrue
、"abc" === "abc"
はtrue
なので、同じ値として扱われ、重複は排除されます。 - オブジェクト型:
{ a: 1 } === { a: 1 }
はfalse
、[1, 2] === [1, 2]
はfalse
です。これは、たとえ中身が同じに見えても、これらがメモリ上で異なる場所に格納された別々のオブジェクトであるためです。
以下のコードで確認してみましょう。
“`javascript
// オブジェクトを含む配列
const objects = [
{ id: 1, name: “Apple” },
{ id: 2, name: “Banana” },
{ id: 1, name: “Apple” }, // idもnameも同じだが、別のオブジェクト
{ id: 3, name: “Orange” },
{ id: 2, name: “Banana” } // idもnameも同じだが、別のオブジェクト
];
const uniqueObjects = […new Set(objects)];
console.log(“\n— Setによるオブジェクトの重複判定 —“);
console.log(objects); // 元の配列
/
[
{ id: 1, name: “Apple” },
{ id: 2, name: “Banana” },
{ id: 1, name: “Apple” }, // 見た目は同じだが別参照
{ id: 3, name: “Orange” },
{ id: 2, name: “Banana” } // 見た目は同じだが別参照
]
/
console.log(uniqueObjects); // Setによって削除された重複はない!
/
[
{ id: 1, name: “Apple” }, // 参照1
{ id: 2, name: “Banana” }, // 参照2
{ id: 1, name: “Apple” }, // 参照3 (参照1とは異なる)
{ id: 3, name: “Orange” }, // 参照4
{ id: 2, name: “Banana” } // 参照5 (参照2とは異なる)
]
/
“`
このように、Set
はプリミティブ型の重複削除には非常に強力ですが、オブジェクトの場合は「参照」が異なるものは重複とみなされません。もし「特定のプロパティ(例: id
)が同じであれば重複とみなしたい」という場合は、Setをそのまま使うのではなく、後述する別の方法(主にMap)を検討する必要があります。
3.7. Setを使った応用例:ユニークな値のカウント
Setを使えば、配列に含まれるユニークな値の数を簡単に知ることができます。Set
オブジェクトにはsize
プロパティがあるため、これを利用します。
“`javascript
const data = [1, 2, 3, 4, 2, 1, 5, 6, 3, 7, 8, 8, 9, 10, 4];
const uniqueDataCount = new Set(data).size;
console.log(“\n— Setを使ったユニークな値のカウント —“);
console.log(元の配列: ${data}
);
console.log(ユニークな値の数: ${uniqueDataCount}
); // 出力: 10
“`
このように、Setは重複削除だけでなく、ユニークな要素に関する様々な操作にも応用できます。
4. 方法2:filter()メソッドとindexOf()メソッドを使う
filter()
メソッドは、配列の各要素に対して指定された関数(コールバック関数)を実行し、その関数がtrue
を返した要素のみを集めて新しい配列を作成するメソッドです。indexOf()
メソッドは、配列の中から指定された要素が最初に出現する位置(インデックス)を返します。この二つを組み合わせることで、重複削除を実現できます。
4.1. filter()メソッドとは?
array.filter(callback(element, index, array))
callback
: 配列の各要素に対して実行される関数です。element
: 現在処理されている要素。index
: 現在処理されている要素のインデックス。array
:filter
が実行されている元の配列。
filter
メソッドは、コールバック関数がtrue
を返した要素だけで構成される新しい配列を返します。元の配列は変更されません。
4.2. indexOf()メソッドとは?
array.indexOf(searchElement, fromIndex)
searchElement
: 検索したい要素。fromIndex
(オプション): 検索を開始するインデックス。省略された場合は0(配列の先頭)から検索します。indexOf
メソッドは、searchElement
が配列の中で最初に出現するインデックスを返します。もし要素が見つからない場合は-1
を返します。
4.3. この2つを組み合わせる原理
配列をfilter
メソッドで処理する際に、各要素について以下の条件をチェックします。
「現在の要素のインデックス」と「その要素が配列全体の中で最初に出現するインデックス」が同じかどうか?
もしこの二つのインデックスが同じであれば、それは配列の中でその要素が最初に出現する場所であり、重複ではありません。もし異なっていれば、それはそれ以前にも同じ要素が出現していた、つまり重複であると判断できます。
filter
メソッドのコールバック関数は、現在の要素(element
)とそのインデックス(index
)を受け取ります。コールバック関数の中で、originalArray.indexOf(element)
を呼び出し、それが現在のインデックス index
と等しいかどうかを判定します。
originalArray.indexOf(element) === index
この条件がtrue
を返す要素だけが、filter
によって新しい配列に残されます。
4.4. 具体的なコード例
“`javascript
// 重複を含む配列
const numbers = [1, 2, 3, 4, 2, 1, 5, 6, 3, 7];
const fruits = [“apple”, “banana”, “orange”, “apple”, “mango”, “banana”];
// filter()とindexOf()を使って重複を削除
const uniqueNumbers = numbers.filter((element, index, array) => {
// element: 現在の要素 (例: 1, 2, 3, 4, 2, …)
// index: 現在のインデックス (例: 0, 1, 2, 3, 4, …)
// array: 元の配列 ([1, 2, 3, 4, 2, 1, 5, 6, 3, 7])
// 配列全体でelementが最初に出現するインデックスを取得
const firstIndex = array.indexOf(element);
// 現在のインデックスが最初に出現するインデックスと一致するか判定
// 一致すればユニーク(最初の出現)、一致しなければ重複(2回目以降の出現)
return firstIndex === index;
});
const uniqueFruits = fruits.filter((element, index, array) => {
return array.indexOf(element) === index;
});
console.log(“\n— filter() + indexOf() —“);
console.log(numbers); // 元の配列は変更されない: [1, 2, 3, 4, 2, 1, 5, 6, 3, 7]
console.log(uniqueNumbers); // 重複削除後の新しい配列: [1, 2, 3, 4, 5, 6, 7]
console.log(fruits); // 元の配列は変更されない: [“apple”, “banana”, “orange”, “apple”, “mango”, “banana”]
console.log(uniqueFruits); // 重複削除後の新しい配列: [“apple”, “banana”, “orange”, “mango”]
“`
コールバック関数は、element
とindex
だけを受け取る省略記法で書かれることが多いです。その場合、元の配列はコールバック関数の第三引数で受け取るか、スコープ外の変数として参照します。
javascript
// より一般的な省略記法
const uniqueNumbersShort = numbers.filter((element, index) => numbers.indexOf(element) === index);
console.log("\n--- filter() + indexOf() (省略記法) ---");
console.log(uniqueNumbersShort); // [1, 2, 3, 4, 5, 6, 7]
4.5. filter() + indexOf() のPros (メリット)
- 理解しやすさ: ロジック(最初の出現位置かどうかで判断する)が比較的直感的で分かりやすいです。
- 環境互換性:
filter()
もindexOf()
も古くからあるメソッドなので、多くのJavaScript環境で特別な対応なしに動作します。 - プリミティブ型の重複削除: プリミティブ型の重複削除には問題なく利用できます。
4.6. filter() + indexOf() のCons (デメリット)
- パフォーマンス: 配列のサイズが大きくなるにつれて、Setを使う方法よりもパフォーマンスが低下します。これは、
filter
が配列の各要素に対して一度ループする O(n) の処理であり、その内部で呼び出されるindexOf
も、最悪の場合配列全体を検索するため O(n) の処理になるからです。結果として、全体の計算量が O(n^2) になります。 - オブジェクトの重複判定: Setと同様に、
indexOf
もオブジェクトの等価性を参照で判断します。そのため、内容が同じでも参照が異なるオブジェクトは重複とみなされず、削除されません。
4.7. indexOfの探索範囲について
indexOf()
は、デフォルトでは配列の先頭(インデックス0)から検索を開始します。これが、filter
と組み合わせた際に重複を判定できる鍵です。
例えば、配列 [1, 2, 1, 3]
を考えます。
element = 1
,index = 0
:indexOf(1)
は 0 を返します。0 === 0
はtrue
なので、1 (最初の出現) は残されます。element = 2
,index = 1
:indexOf(2)
は 1 を返します。1 === 1
はtrue
なので、2 (最初の出現) は残されます。element = 1
,index = 2
:indexOf(1)
は 0 を返します。0 === 2
はfalse
なので、1 (2回目の出現) は削除されます。element = 3
,index = 3
:indexOf(3)
は 3 を返します。3 === 3
はtrue
なので、3 (最初の出現) は残されます。
結果として、[1, 2, 3]
という重複が削除された配列が得られます。
4.8. filter() + indexOf() を使った応用例
特定の条件を満たす要素に対してこの方法を使うことも可能です。
“`javascript
// 数字と文字列が混ざった配列
const mixedArray = [1, ‘a’, 2, ‘b’, 1, ‘a’, 3, ‘c’, 2];
// 数値のみを対象に重複削除する(この方法では難しい。結局全体で判定になる)
// これはfilter + indexOfの直接の応用例としては適切ではありません。
// filter + indexOfは、配列全体の要素に対して重複を判定します。
// 特定の型の重複だけを削除したい場合は、先に型でフィルタリングするか、
// オブジェクト配列の重複削除で使うようなカスタム判定ロジックが必要です。
// 純粋に配列全体の重複を削除する例としては有効
const uniqueMixed = mixedArray.filter((element, index) => mixedArray.indexOf(element) === index);
console.log(“\n— filter() + indexOf() (混在配列) —“);
console.log(uniqueMixed); // [1, ‘a’, 2, ‘b’, 3, ‘c’]
“`
この方法もオブジェクトの重複判定には不向きである点に注意が必要です。
5. 方法3:filter()メソッドとincludes()メソッドを使う(非推奨)
この方法は理解のためだけに紹介します。Setやfilter + indexOfに比べて非常に効率が悪く、通常は使うべきではありません。
5.1. includes()メソッドとは?
array.includes(searchElement, fromIndex)
searchElement
: 検索したい要素。fromIndex
(オプション): 検索を開始するインデックス。デフォルトは0。includes
メソッドは、配列にsearchElement
が含まれているかどうかをtrue
またはfalse
で返します。
5.2. この2つを組み合わせる原理
filter
メソッドのコールバック関数の中で、重複を削除した新しい配列(または現在構築中の配列)に、現在の要素が既に含まれているかをincludes()
を使ってチェックします。
- もし含まれていなければ(
includes
がfalse
を返せば)、それは新しい配列にとってはユニークな要素なので、filter
はその要素を残します。 - もし既に含まれていれば(
includes
がtrue
を返せば)、それは重複なので、filter
はその要素を削除します。
5.3. 具体的なコード例
“`javascript
// 重複を含む配列
const numbers = [1, 2, 3, 4, 2, 1, 5, 6, 3, 7];
// filter()とincludes()を使って重複を削除 (非推奨)
const uniqueNumbers = numbers.filter((element, index, array) => {
// array.slice(0, index) で、現在の要素より「前」の要素だけの配列を取得
const previousElements = array.slice(0, index);
// previousElementsに現在の要素が含まれていないかチェック
// 含まれていなければ(includesがfalse)、それは最初の出現
return !previousElements.includes(element);
});
console.log(“\n— filter() + includes() (非推奨) —“);
console.log(numbers); // 元の配列は変更されない
console.log(uniqueNumbers); // 重複削除後の新しい配列: [1, 2, 3, 4, 5, 6, 7]
“`
もう一つの書き方として、filterのコールバック関数内で、ユニークな要素を格納する外部の配列を用意しておき、そこにincludes
でチェックしながら追加していく、というロジックをfilterで実現しようとすることも考えられます。ただし、filterは新しい配列を返すメソッドであり、コールバック関数は元の配列の要素に対してのみ実行されるため、上記のようにslice(0, index)
を使って「それより前の要素」だけを調べるのが一般的な(そしてやはり非効率な)実装になります。
5.4. なぜ非推奨なのか?
- パフォーマンス: この方法も非常に効率が悪いです。
filter
は配列の各要素に対してループします (O(n))。その内部で、slice(0, index)
はその時点までの部分配列を生成し (O(index))、includes()
はその部分配列を検索します (最悪 O(index))。結果として、全体の計算量は O(n^2) よりも悪化する可能性があり、非常に遅くなります。特に配列が大きくなると実用的ではありません。 - 可読性: filter + indexOf に比べて、なぜこれで重複が削除できるのか直感的に分かりにくいです。
この方法は、アルゴリズムの比較として理解しておくのは良いですが、実際のコードで使うことは避けるべきです。
6. 方法4:forループまたはforEach()メソッドを使う
手動で新しい配列を作成し、そこに重複していない要素だけを追加していく方法です。古くから使われている基本的な手法であり、他のメソッドが使えない環境や、処理内容をより細かく制御したい場合に有効です。
6.1. 手動で新しい配列を作成する方法
- 重複を削除した結果を格納するための空の配列を用意します。
- 元の配列の要素を一つずつループで確認します。
- 各要素について、作成中の新しい配列にその要素が既に含まれているかどうかをチェックします。
- もし含まれていなければ、その要素はユニークであると判断し、新しい配列に追加します。
- もし既に含まれていれば、それは重複なのでスキップします。
要素が新しい配列に既に含まれているかどうかのチェックには、indexOf()
やincludes()
メソッドを使います。
6.2. forループを使った実装例 (includesを使用)
“`javascript
// 重複を含む配列
const numbers = [1, 2, 3, 4, 2, 1, 5, 6, 3, 7];
// forループとincludes()を使って重複を削除
const uniqueNumbers = []; // 結果を格納する新しい配列
for (let i = 0; i < numbers.length; i++) {
const element = numbers[i];
// 新しい配列に現在の要素がまだ含まれていないかチェック
if (!uniqueNumbers.includes(element)) {
// 含まれていなければ追加
uniqueNumbers.push(element);
}
}
console.log(“\n— forループ + includes() —“);
console.log(numbers); // 元の配列は変更されない
console.log(uniqueNumbers); // 重複削除後の新しい配列: [1, 2, 3, 4, 5, 6, 7]
“`
6.3. forEach()を使った実装例 (includesを使用)
forEach()
メソッドは、配列の各要素に対して指定された関数を一度だけ実行します。ループ処理を関数型プログラミング的に記述できます。
“`javascript
// 重複を含む配列
const fruits = [“apple”, “banana”, “orange”, “apple”, “mango”, “banana”];
// forEach()とincludes()を使って重複を削除
const uniqueFruits = []; // 結果を格納する新しい配列
fruits.forEach(element => {
// 新しい配列に現在の要素がまだ含まれていないかチェック
if (!uniqueFruits.includes(element)) {
// 含まれていなければ追加
uniqueFruits.push(element);
}
});
console.log(“\n— forEach() + includes() —“);
console.log(fruits); // 元の配列は変更されない
console.log(uniqueFruits); // 重複削除後の新しい配列: [“apple”, “banana”, “orange”, “mango”]
“`
6.4. indexOf()を使う場合 vs includes()を使う場合
上記コード例ではincludes()
を使いましたが、indexOf()
を使って「新しい配列に要素が含まれていないこと(indexOfが-1を返すこと)」をチェックしても同じ結果が得られます。
javascript
const numbers = [1, 2, 3, 4, 2, 1, 5, 6, 3, 7];
const uniqueNumbers = [];
for (let i = 0; i < numbers.length; i++) {
const element = numbers[i];
// 新しい配列に現在の要素が含まれていないかチェック
// indexOfが-1を返せば、その要素はまだ含まれていない
if (uniqueNumbers.indexOf(element) === -1) {
uniqueNumbers.push(element);
}
}
console.log("\n--- forループ + indexOf() ---");
console.log(uniqueNumbers); // [1, 2, 3, 4, 5, 6, 7]
機能的な違いはほぼありませんが、includes()
は論理値を返すため、より直感的に「含まれているかどうか」を表現できます。パフォーマンス面でも、内部的には類似の探索を行いますが、大きな違いはありません。どちらも、構築中のuniqueNumbers
配列に対して要素が含まれているかチェックするため、最悪の場合、外側のループが O(n)、内側のチェックが O(n) となり、全体の計算量は O(n^2) となります。これは、filter + indexOf と同等、あるいはfilter + includes よりはマシですが、Setや後述するMapに比べると効率が悪いです。
6.5. forループ/forEach()メソッドのPros (メリット)
- 原理の分かりやすさ: 手続き的な処理なので、一つずつ要素を確認し、新しい配列に追加していくロジックが最も直感的で理解しやすいかもしれません。
- 環境互換性:
for
ループはJavaScriptの基本的な構文であり、forEach()
、indexOf()
、includes()
も比較的古くから利用可能です(includes
はES7ですが、IEを除くモダンブラウザではサポートされています)。 - 柔軟性: ループの中で他の処理(例:要素の加工、ログ出力など)を同時に行いやすいという柔軟性があります。
6.6. forループ/forEach()メソッドのCons (デメリット)
- コード量: Setやfilterを使った方法に比べて、書くべきコードの量が多くなります。
- パフォーマンス: 前述の通り、ネストされた探索(外側のループと、内側の
indexOf
またはincludes
)が発生するため、O(n^2)の計算量となり、大きな配列の処理には向きません。 - オブジェクトの重複判定: やはり
indexOf
やincludes
はオブジェクトの等価性を参照で判断するため、内容が同じでも参照が異なるオブジェクトは重複とみなされません。
この方法は、学習目的や、ごく小さな配列を処理する場合、あるいはパフォーマンスがそれほど重要でない場合に適しています。
7. 方法5:Mapオブジェクトを使う (オブジェクトの重複判定に強力)
Setと同様に、Map
オブジェクトもES6で導入されました。Map
はキーと値のペアのコレクションであり、キーはあらゆる型(オブジェクトを含む)を持つことができます。このMap
を利用して、特にオブジェクトを含む配列の重複を特定のプロパティに基づいて削除する場合に強力な方法を実装できます。
7.1. Mapオブジェクトとは?
Map
はオブジェクトと似ていますが、いくつかの重要な違いがあります。
- キーはプリミティブ値だけでなく、オブジェクトなども含めあらゆる型を指定できます。
- キーの順序が保持されます(挿入順)。
size
プロパティで要素数を簡単に取得できます。- 反復処理が簡単です。
7.2. Mapを使った重複削除の原理
特にオブジェクト配列の重複削除に焦点を当てます。
- 重複を削除した結果を格納するための
Map
オブジェクトを作成します。 - 元の配列の要素を一つずつループで確認します。
- 各要素について、重複判定の基準となる特定のプロパティの値をMapのキーとして使用し、元のオブジェクト全体をMapの値として格納します。
- Mapのキーは一意でなければならないため、同じプロパティ値を持つオブジェクトを
set()
しようとしても、Mapには最初に設定された(または後から上書きされた)オブジェクトしか残りません。結果として、指定したプロパティ値が重複するオブジェクトがMapから排除されます。 - ループが終了したら、Mapの
values()
メソッドを使って、Mapに格納されている値(つまり、重複が削除されたユニークなオブジェクト)のイテレーターを取得します。 - 最後に、
Array.from()
やスプレッド構文を使って、このイテレーターから新しい配列を作成します。
7.3. 具体的なコード例 (オブジェクト配列)
IDが同じユーザーオブジェクトを重複とみなして削除する例です。
“`javascript
// 重複を含むオブジェクト配列 (idが重複)
const users = [
{ id: 1, name: “Alice”, age: 30 },
{ id: 2, name: “Bob”, age: 25 },
{ id: 1, name: “Alice”, age: 30 }, // id: 1 は重複
{ id: 3, name: “Charlie”, age: 35 },
{ id: 2, name: “Bob”, age: 26 } // id: 2 は重複 (ageは違うがidが同じ)
];
// Mapを使ってidを基準に重複を削除
const uniqueUsersMap = new Map(); // Mapを作成
users.forEach(user => {
// user.id をキーに、userオブジェクト全体を値としてMapにセット
// 同じidがあれば、Mapの値が上書きされる(古いオブジェクトがMapから削除される)
uniqueUsersMap.set(user.id, user);
});
// Mapのvalues()イテレーターから配列を作成
const uniqueUsers = Array.from(uniqueUsersMap.values());
// またはスプレッド構文: const uniqueUsers = […uniqueUsersMap.values()];
console.log(“\n— Mapを使ったオブジェクト配列の重複削除 (id基準) —“);
console.log(users); // 元の配列は変更されない
/
[
{ id: 1, name: “Alice”, age: 30 }, // 最初に見つかった id: 1 のオブジェクト
{ id: 2, name: “Bob”, age: 26 }, // 最後に見つかった id: 2 のオブジェクト
{ id: 3, name: “Charlie”, age: 35 } // id: 3 のオブジェクト
]
/
console.log(uniqueUsers); // 重複削除後の新しい配列
“`
注意: この例では、同じid
を持つオブジェクトが複数存在する場合、最後にset()
されたオブジェクトがMapに残ります。もし最初に見つかったオブジェクトを残したい場合は、map.has()
でキーの存在を確認してからset()
するかどうかを判断します。
“`javascript
// 最初に見つかったオブジェクトを残す場合
const uniqueUsersMapFirst = new Map();
users.forEach(user => {
// uniqueUsersMapFirstにまだuser.idというキーがない場合のみセット
if (!uniqueUsersMapFirst.has(user.id)) {
uniqueUsersMapFirst.set(user.id, user);
}
});
const uniqueUsersFirst = […uniqueUsersMapFirst.values()];
console.log(“\n— Mapを使ったオブジェクト配列の重複削除 (id基準、最初を保持) —“);
/
[
{ id: 1, name: “Alice”, age: 30 }, // 最初に見つかった id: 1 のオブジェクト
{ id: 2, name: “Bob”, age: 25 }, // 最初に見つかった id: 2 のオブジェクト
{ id: 3, name: “Charlie”, age: 35 } // id: 3 のオブジェクト
]
/
console.log(uniqueUsersFirst); // 重複削除後の新しい配列
“`
プリミティブ型の重複削除にもMapは使えますが、Setの方がよりシンプルで一般的です。
“`javascript
// Mapを使ったプリミティブ型の重複削除 (Setの方が良いが例として)
const numbers = [1, 2, 3, 4, 2, 1, 5, 6, 3, 7];
const uniqueNumbersMap = new Map();
numbers.forEach(num => {
uniqueNumbersMap.set(num, true); // 値は何でも良い、キーが一意であればOK
});
const uniqueNumbers = […uniqueNumbersMap.keys()]; // または values() から作成しても同じ結果
console.log(“\n— Mapを使ったプリミティブ型の重複削除 —“);
console.log(uniqueNumbers); // [1, 2, 3, 4, 5, 6, 7]
“`
7.4. Mapを使った重複削除のPros (メリット)
- オブジェクトの重複判定に強力: 特定のプロパティや、複数のプロパティを組み合わせた複合キー(例:
'${user.id}-${user.name}'
のような文字列キー)で重複を判断できます。 - パフォーマンス: Mapの
set
やhas
操作は平均的にO(1)の計算量で行えます。配列の各要素に対してMap操作を行うため、全体の計算量はO(n)となり、Setと同様に高速です。 - 柔軟性: キーとしてあらゆる型を使えるため、より複雑な重複判定ロジックを実装するのに適しています。
7.5. Mapを使った重複削除のCons (デメリット)
- シンプルさ(Setと比較して): プリミティブ型の重複削除だけを考えればSetの方がコードが短くシンプルです。Mapはオブジェクトの重複削除など、より複雑なケースで真価を発揮します。
- 環境依存性: Setと同様にES6で導入されたため、古い環境での互換性に注意が必要です。
7.6. Mapを使った応用例:重複しない要素のグループ化
Mapを使えば、重複を削除するだけでなく、同じキーを持つ要素をグループ化するといった応用も可能です。
“`javascript
// カテゴリごとに商品をグループ化する例
const products = [
{ id: 1, name: “Laptop”, category: “Electronics” },
{ id: 2, name: “Book”, category: “Books” },
{ id: 3, name: “Tablet”, category: “Electronics” },
{ id: 4, name: “Notebook”, category: “Books” },
{ id: 5, name: “Mouse”, category: “Electronics” }
];
const productsByCategory = new Map();
products.forEach(product => {
const category = product.category;
// Mapにそのカテゴリがキーとして存在するか確認
if (!productsByCategory.has(category)) {
// なければ、そのカテゴリをキーとして空の配列をセット
productsByCategory.set(category, []);
}
// そのカテゴリの配列に現在の商品を追加
productsByCategory.get(category).push(product);
});
console.log(“\n— Mapを使った要素のグループ化 (カテゴリ基準) —“);
console.log(productsByCategory);
/
Map(2) {
‘Electronics’ => [
{ id: 1, name: “Laptop”, category: “Electronics” },
{ id: 3, name: “Tablet”, category: “Electronics” },
{ id: 5, name: “Mouse”, category: “Electronics” }
],
‘Books’ => [
{ id: 2, name: “Book”, category: “Books” },
{ id: 4, name: “Notebook”, category: “Books” }
]
}
/
// Mapから配列の配列として結果を取り出すことも可能
const groupedProductsArray = Array.from(productsByCategory.entries());
console.log(groupedProductsArray);
/
[
[ ‘Electronics’, [ { id: 1, … }, { id: 3, … }, { id: 5, … } ] ],
[ ‘Books’, [ { id: 2, … }, { id: 4, … } ] ]
]
/
“`
これは直接の重複削除ではありませんが、Mapの強力な機能を示す例であり、重複判定をキーに使うという考え方の延長線上にあるテクニックです。
8. 方法6:reduce()メソッドを使う
reduce()
メソッドは、配列の各要素を処理して、単一の値(この場合は重複を削除した新しい配列)に集約するために使われます。関数型プログラミングのスタイルで重複削除を記述する方法です。
8.1. reduce()メソッドとは?
array.reduce(callback(accumulator, currentValue, currentIndex, array), initialValue)
callback
: 配列の各要素に対して実行される関数です。accumulator
: 累積値。コールバック関数の戻り値が次の呼び出し時のaccumulatorになります。currentValue
: 現在処理されている要素。currentIndex
: 現在処理されている要素のインデックス。array
:reduce
が実行されている元の配列。
initialValue
(オプション): 累積値の初期値。指定しない場合、配列の最初の要素がinitialValueとなり、処理は2番目の要素から開始されます。重複削除の場合は、通常空の配列をinitialValue
として指定します。reduce
メソッドは、最終的な累積値(重複削除された配列)を返します。元の配列は変更されません。
8.2. reduceを使った重複削除の原理
reduce
メソッドを使って、重複を削除した新しい配列をaccumulator
として構築していきます。配列の各要素(currentValue
)をaccumulator
に追加する前に、accumulator
にその要素が既に含まれているかを確認します。
- もし
accumulator
にまだ含まれていなければ(ユニーク)、currentValue
をaccumulator
に追加し、新しいaccumulator
として返します。 - もし既に含まれていれば(重複)、
currentValue
はスキップし、現在のaccumulator
をそのまま返します。
含まれているかどうかのチェックには、indexOf()
またはincludes()
メソッドを使います。
8.3. 具体的なコード例 (indexOfまたはincludesを使用)
“`javascript
// 重複を含む配列
const numbers = [1, 2, 3, 4, 2, 1, 5, 6, 3, 7];
// reduce()とindexOf()を使って重複を削除
const uniqueNumbersReduce = numbers.reduce((accumulator, currentValue) => {
// accumulator: 現在構築中のユニークな要素の配列
// currentValue: 現在処理している要素
// accumulatorに現在の要素が含まれていないかチェック
if (accumulator.indexOf(currentValue) === -1) {
// 含まれていなければ、accumulatorに要素を追加した新しい配列を作成して返す
return accumulator.concat(currentValue); // concatは新しい配列を作成
// あるいは、破壊的メソッドpushを使ってから返す
// accumulator.push(currentValue);
// return accumulator;
} else {
// 含まれていれば、accumulatorをそのまま返す(要素を追加しない)
return accumulator;
}
}, []); // 累積値の初期値は空の配列
console.log(“\n— reduce() + indexOf() —“);
console.log(numbers); // 元の配列は変更されない
console.log(uniqueNumbersReduce); // 重複削除後の新しい配列: [1, 2, 3, 4, 5, 6, 7]
// includes()を使った場合
const uniqueNumbersReduceIncludes = numbers.reduce((accumulator, currentValue) => {
if (!accumulator.includes(currentValue)) {
return accumulator.concat(currentValue); // 非破壊的な追加
// あるいは破壊的な追加: accumulator.push(currentValue); return accumulator;
} else {
return accumulator;
}
}, []);
console.log(“\n— reduce() + includes() —“);
console.log(uniqueNumbersReduceIncludes); // [1, 2, 3, 4, 5, 6, 7]
“`
concat()
を使うと新しい配列が生成されるため、各ステップで新しい配列が返されます(非破壊的ですが、パフォーマンスはやや劣る可能性があります)。push()
を使うと、accumulator
である配列自体が変更されます(破壊的ですが、reduce
の累積値としてはその変更された配列が次のステップに渡されます)。reduce
メソッド自体は新しい値を返しますが、コールバック内で参照型のaccumulatorを破壊的に変更しても問題なく動作することが多いです。パフォーマンスを重視する場合はpush()
を使うことが多いでしょう。
8.4. reduce()を使った重複削除のPros (メリット)
- 関数型プログラミング: reduceを使うことで、配列処理を宣言的なスタイルで記述できます。
- 柔軟性: reduceは様々な配列処理に使える汎用性の高いメソッドです。
8.5. reduce()を使った重複削除のCons (デメリット)
- 理解の難しさ: 初心者にとっては、accumulatorの概念やコールバック関数の振る舞いを理解するのが難しい場合があります。
- パフォーマンス:
indexOf
やincludes
を使う場合、forループやfilter + indexOf/includes と同様に O(n^2) の計算量となり、パフォーマンスはSetやMapに劣ります。 - オブジェクトの重複判定: やはり
indexOf
やincludes
はオブジェクトの等価性を参照で判断するため、オブジェクトの重複削除には向きません。
8.6. reduceとSetを組み合わせる方法 (より効率的)
reduceの初期値にSet
オブジェクトを指定し、Set
の特性を利用して効率的に重複を削除し、最後にSetを配列に戻す方法です。これはSet単独の方法をreduceで記述しただけなので、Setを使う方法と本質的には同じですが、reduceの使い方のバリエーションとして知っておくと良いでしょう。
“`javascript
// 重複を含む配列
const numbers = [1, 2, 3, 4, 2, 1, 5, 6, 3, 7];
// reduce()とSetを使って重複を削除
const uniqueNumbersReduceSet = numbers.reduce((accumulatorSet, currentValue) => {
// accumulatorSet: 現在構築中のSetオブジェクト
// currentValue: 現在処理している要素
// Setに要素を追加 (重複は自動的に排除される)
accumulatorSet.add(currentValue);
// 変更されたSetを次のステップに渡す
return accumulatorSet;
}, new Set()); // 累積値の初期値は空のSet
// 最終的にSetになった累積値から配列を作成
const uniqueNumbersArray = […uniqueNumbersReduceSet];
console.log(“\n— reduce() + Set —“);
console.log(numbers); // 元の配列は変更されない
console.log(uniqueNumbersArray); // 重複削除後の新しい配列: [1, 2, 3, 4, 5, 6, 7]
“`
この方法は、Setの効率性(O(n))とreduceの記述スタイルを組み合わせたものです。プリミティブ型の重複削除には Set単独の方法が最もシンプルですが、reduceに慣れている場合はこのような書き方も可能です。
9. オブジェクトを含む配列の重複削除
前述のように、JavaScriptのデフォルトの等価性判定(特にSetやindexOf
/includes
が内部で使う参照等価性)では、内容が同じでも参照が異なるオブジェクトは重複とみなされません。
“`javascript
const obj1 = { id: 1, value: ‘a’ };
const obj2 = { id: 2, value: ‘b’ };
const obj3 = { id: 1, value: ‘a’ }; // obj1 と内容は同じだが別オブジェクト
console.log(obj1 === obj3); // false
“`
オブジェクト配列の重複削除で重要なのは、「何を基準に重複とみなすか」を明確に定義することです。多くの場合、オブジェクトの特定のプロパティ(例: id
、uuid
、メールアドレスなど、ユニークであることが期待される値)を基準とします。
ここでは、特定のプロパティ(例: id
)に基づいてオブジェクトの重複を削除する方法をいくつか紹介します。
9.1. 方法A: Mapを使う (最も推奨)
前述の「方法5:Mapオブジェクトを使う」で詳しく解説しました。Mapのキーに重複判定の基準となるプロパティ値を使い、値に元のオブジェクトを格納する方法が最も効率的(O(n))で、かつ特定のプロパティを基準に重複を削除したいというニーズに合致します。
コード例 (再掲、最初に見つかったオブジェクトを保持する場合):
“`javascript
const users = [
{ id: 1, name: “Alice”, age: 30 },
{ id: 2, name: “Bob”, age: 25 },
{ id: 1, name: “Alice”, age: 30 }, // id: 1 重複
{ id: 3, name: “Charlie”, age: 35 },
{ id: 2, name: “Bob”, age: 26 } // id: 2 重複
];
const uniqueUsersMap = new Map();
users.forEach(user => {
// uniqueUsersMapにuser.idというキーがまだない場合のみセット
if (!uniqueUsersMap.has(user.id)) {
uniqueUsersMap.set(user.id, user);
}
});
const uniqueUsers = […uniqueUsersMap.values()];
console.log(“\n— オブジェクト配列の重複削除: Map (id基準) —“);
console.log(uniqueUsers);
/
[
{ id: 1, name: “Alice”, age: 30 }, // 最初に見つかった id: 1 のオブジェクト
{ id: 2, name: “Bob”, age: 25 }, // 最初に見つかった id: 2 のオブジェクト
{ id: 3, name: “Charlie”, age: 35 } // id: 3 のオブジェクト
]
/
“`
これはオブジェクト配列の重複削除において、パフォーマンス、コードの意図の明確さ、および柔軟性のバランスが最も優れている方法と言えます。
9.2. 方法B: SetとJSON.stringify()を使う (単純だが注意が必要)
オブジェクトを一旦JSON文字列に変換することで、オブジェクトの「値」(プロパティとその値の組み合わせ)を文字列として表現し、その文字列の重複をSetで削除するという方法です。
“`javascript
const data = [
{ id: 1, value: ‘a’ },
{ id: 2, value: ‘b’ },
{ id: 1, value: ‘a’ }, // 内容は同じ
{ id: 3, value: ‘c’ },
{ id: 2, value: ‘b’ } // 内容は同じ
];
// オブジェクトをJSON文字列に変換してからSetで重複削除
const uniqueStrings = new Set(data.map(obj => JSON.stringify(obj)));
// JSON文字列のSetから元のオブジェクトに戻す
const uniqueObjects = Array.from(uniqueStrings).map(str => JSON.parse(str));
console.log(“\n— オブジェクト配列の重複削除: Set + JSON.stringify() —“);
console.log(uniqueObjects);
/
[
{ id: 1, value: ‘a’ },
{ id: 2, value: ‘b’ },
{ id: 3, value: ‘c’ }
]
/
“`
Pros (メリット):
- コードが比較的単純です。
- 特定のプロパティだけでなく、オブジェクト全体の構造と値を基準に重複を判断できます。
Cons (デメリット) – 非常に重要**:
- プロパティの順序:
JSON.stringify()
はオブジェクトのプロパティの順序を保証しません(実装依存の場合があるが、一般的に順序は無視されるべき)。{ a: 1, b: 2 }
と{ b: 2, a: 1 }
は同じオブジェクトを表現していますが、JSON.stringify()
の結果が必ずしも同じ文字列になるとは限りません(多くの場合なりますが、仕様上保証されていませんし、ネストされたオブジェクトなどで予期せぬ挙動をする可能性もあります)。もし順序が異なると、Setは異なる文字列と判断してしまい、重複が正しく削除されません。 - データ型の制限:
JSON.stringify()
はすべてのJavaScriptの値を扱えるわけではありません。Date
オブジェクトは文字列に変換されますが、関数やundefined
、Symbol
、BigInt
などは無視されるかエラーになります。循環参照があるオブジェクトでもエラーになります。 - パフォーマンス: 大きなオブジェクトや配列の場合、
JSON.stringify()
とJSON.parse()
の処理はコストがかかる可能性があります。 - 元のオブジェクトの喪失: JSON化とパースを経るため、元のオブジェクト参照や、JSONで表現できないプロパティ(メソッドなど)は失われます。
この方法は、シンプルでフラットなデータオブジェクトを含む配列で、かつパフォーマンスやデータ型の制限が問題にならない場合に限定的に利用できます。
9.3. 方法C: filter()とfind()/some()を使う (パフォーマンスが悪い)
filterとfind/someを組み合わせて、構築中の新しい配列に、現在の要素と同じプロパティ値を持つ要素が既に存在するかをチェックする方法です。原理的にはforループ + includes/indexOf と似ています。
“`javascript
const users = [
{ id: 1, name: “Alice”, age: 30 },
{ id: 2, name: “Bob”, age: 25 },
{ id: 1, name: “Alice”, age: 30 }, // id: 1 重複
{ id: 3, name: “Charlie”, age: 35 },
{ id: 2, name: “Bob”, age: 26 } // id: 2 重複
];
// filter()とfind()を使ってidを基準に重複を削除
const uniqueUsers = users.filter((user, index, self) => {
// self.findIndex() で、現在のuserと同じidを持つ最初の要素のインデックスを探す
// もしそのインデックスが現在のindexと一致すれば、それは最初の出現
// findIndex() はコールバック関数がtrueを返す最初のインデックスを返す
return self.findIndex(u => u.id === user.id) === index;
});
console.log(“\n— オブジェクト配列の重複削除: filter() + findIndex() —“);
console.log(uniqueUsers);
/
[
{ id: 1, name: “Alice”, age: 30 }, // 最初に見つかった id: 1 のオブジェクト
{ id: 2, name: “Bob”, age: 25 }, // 最初に見つかった id: 2 のオブジェクト
{ id: 3, name: “Charlie”, age: 35 } // id: 3 のオブジェクト
]
/
“`
あるいは、some()
メソッドを使って、現在の要素より前の部分配列に同じIDを持つ要素が既に含まれているかチェックする方法も考えられます。
“`javascript
const uniqueUsersSome = users.filter((user, index, self) => {
// slice(0, index) で現在の要素より前の部分配列を取得
// some() で、その部分配列に現在のuserと同じidを持つ要素が一つでもあるかチェック
// some() が false を返せば、それは前の要素に同じIDがなかった(最初の出現)
return !self.slice(0, index).some(u => u.id === user.id);
});
console.log(“\n— オブジェクト配列の重複削除: filter() + slice() + some() —“);
console.log(uniqueUsersSome);
/
[
{ id: 1, name: “Alice”, age: 30 }, // 最初に見つかった id: 1 のオブジェクト
{ id: 2, name: “Bob”, age: 25 }, // 最初に見つかった id: 2 のオブジェクト
{ id: 3, name: “Charlie”, age: 35 } // id: 3 のオブジェクト
]
/
“`
Pros (メリット):
- 特定のプロパティを基準に重複を削除できる。
- MapやSetより古い環境でも動作する可能性がある(
findIndex
/some
/slice
はES6以降だが、filter
はさらに古い)。
Cons (デメリット):
- パフォーマンス:
filter
のループ (O(n)) の内部で、findIndex
やsome
(O(n))、またはslice
(O(index)) が実行されるため、全体の計算量は O(n^2) となり、大きな配列の処理には適しません。SetやMapを使った方法と比較して大幅に遅くなります。 - コードの複雑さ: Mapを使った方法に比べて、コードがやや読み解きにくいかもしれません。
オブジェクト配列の重複削除においては、Mapを使う方法がパフォーマンスとコードのシンプルさのバランスが最も優れているため、強く推奨されます。
10. パフォーマンス比較
これまで見てきた各方法の一般的なパフォーマンス特性をまとめます。計算量は、配列のサイズをnとした場合の、処理にかかる時間の増加度合いを示します。
- Setを使う: O(n)
- Setへの要素の追加・存在チェックが平均的に O(1)
- Mapを使う: O(n)
- Mapへの要素の追加・キーの存在チェックが平均的に O(1)
- オブジェクト配列の特定のプロパティをキーにする場合に特に有効
- filter() + indexOf(): O(n^2)
- filterの O(n) ループ内で indexOf が最悪 O(n) 検索を行う
- forループ / forEach() + indexOf() / includes(): O(n^2)
- 外側の O(n) ループ内で includes/indexOf が構築中の配列を最悪 O(n) 検索を行う
- filter() + includes(): O(n^2) またはそれより悪い
- slice() の生成コストも加わるため
- filter() + findIndex() / some() (オブジェクト配列): O(n^2)
- filter の O(n) ループ内で findIndex/some が最悪 O(n) 検索を行う
- Set + JSON.stringify() (オブジェクト配列): オブジェクトのサイズや構造、JSON化/パースのコストに依存。一般的には O(n * C) (Cはオブジェクトあたりのコスト)。大きなオブジェクトでは遅くなる可能性があり、前述のデメリットも多い。
結論として、特別な理由がない限り、以下の方法を優先的に検討すべきです。
- プリミティブ型の配列: Setを使う (O(n))
- オブジェクトを含む配列(特定のプロパティで重複判定): Mapを使う (O(n))
これらの方法は、配列のサイズが大きくなっても処理時間が比較的線形に増加するため、大規模なデータを扱う場合にパフォーマンス上の大きなメリットがあります。
O(n^2)の方法は、配列のサイズが数十〜数百程度であれば問題にならないことが多いですが、数千、数万件を超えると目に見えて処理が遅くなります。
簡単なベンチマークコード例(Node.js環境など、実行環境によって結果は異なります):
“`javascript
function generateRandomArray(size, maxValue) {
const arr = [];
for (let i = 0; i < size; i++) {
arr.push(Math.floor(Math.random() * maxValue));
}
return arr;
}
const arraySize = 50000; // 比較のために大きめの配列を用意
const randomNumbers = generateRandomArray(arraySize, arraySize / 2); // ある程度重複が含まれるようにする
console.log(\n--- 重複削除方法 パフォーマンス比較 (配列サイズ: ${arraySize}) ---
);
// Setを使う
console.time(“Set”);
const uniqueSet = […new Set(randomNumbers)];
console.timeEnd(“Set”);
// console.log(“Set結果:”, uniqueSet.length);
// filter() + indexOf() を使う
console.time(“filter + indexOf”);
const uniqueFilterIndexOf = randomNumbers.filter((item, index, self) => self.indexOf(item) === index);
console.timeEnd(“filter + indexOf”);
// console.log(“filter + indexOf 結果:”, uniqueFilterIndexOf.length);
// Mapを使う (プリミティブ型なのでMap.set(key, true) の形式)
console.time(“Map (primitive)”);
const uniqueMap = […new Map(randomNumbers.map(item => [item, true])).keys()];
console.timeEnd(“Map (primitive)”);
// console.log(“Map (primitive) 結果:”, uniqueMap.length);
// filter() + slice() + includes() を使う (非推奨なのでコメントアウトまたは非常に小さいサイズでテスト)
/
console.time(“filter + slice + includes”);
const uniqueFilterSliceIncludes = randomNumbers.filter((item, index, self) => !self.slice(0, index).includes(item));
console.timeEnd(“filter + slice + includes”);
console.log(“filter + slice + includes 結果:”, uniqueFilterSliceIncludes.length); // 数が大きいと非常に遅い
/
// forループ + includes() を使う (非推奨なのでコメントアウトまたは非常に小さいサイズでテスト)
/
console.time(“for loop + includes”);
const uniqueForIncludes = [];
for (let i = 0; i < randomNumbers.length; i++) {
if (!uniqueForIncludes.includes(randomNumbers[i])) {
uniqueForIncludes.push(randomNumbers[i]);
}
}
console.timeEnd(“for loop + includes”);
console.log(“for loop + includes 結果:”, uniqueForIncludes.length); // 数が大きいと非常に遅い
/
“`
上記のコードを実行してみると、SetとMapが圧倒的に高速であることが確認できます。
11. 注意点と応用
11.1. 元の配列を変更するか、新しい配列を作成するか (破壊的 vs 非破壊的)
この記事で紹介した多くの方法は、重複を削除した新しい配列を返す非破壊的な方法です (Set
, filter
, reduce
, Map
)。これは、元のデータを保持したまま処理を進められるため、予期せぬ副作用を防ぎ、安全なプログラミングスタイルとされています。
もしどうしても元の配列自体を変更したい(破壊的な変更を行いたい)場合は、いくつかの方法がありますが、一般的には非推奨です。例えば、重複を削除した新しい配列を作成した後、元の配列をその新しい配列で置き換えるのが最も安全な方法です (originalArray = uniqueArray;
)。
どうしても元の配列を直接操作したい場合は、ループ処理中にsplice()
メソッドを使って重複要素を削除するという方法も理論的には可能ですが、ループ中に配列の要素を削除するとインデックスがずれてしまい、非常に複雑でバグの原因になりやすいため、お勧めできません。もし行う場合は、後ろからループするなど注意深い実装が必要です。
11.2. 重複削除後の要素の順序
filter() + indexOf()
forループ / forEach() + indexOf() / includes()
filter() + findIndex() / slice() + some()
(オブジェクト配列)Map
(デフォルトでは挿入順)
これらの方法は、元の配列における最初の出現順序を保持します。
Set
SetはES6の仕様で挿入順を保持することが定められています。そのため、Setを使って配列に戻した場合も、元の配列の要素がSetに追加された順序(つまり最初の出現順序)で新しい配列が構成されます。
Map
(キーを基準にしたオブジェクト配列の場合)
Mapもデフォルトではキーの挿入順を保持しますが、オブジェクト配列をMapのキーとして使うのではなく、オブジェクトのプロパティ値をキーとして使う場合(例:map.set(user.id, user)
)、Mapのキーはuser.id
の順序で格納されます。そしてmap.values()
はキーの挿入順に値を返します。結果的に、新しい配列は元の配列で各id
の最初の出現順序を保持します。
Set + JSON.stringify()
(オブジェクト配列)
JSON文字列に変換する過程で、元のオブジェクトのプロパティ順序が失われる可能性があるため、結果として得られる配列のオブジェクトのプロパティ順序や、配列自体の要素の順序が元の配列の順序と異なる可能性があります。
11.3. null
や undefined
の扱い
null
や undefined
もJavaScriptの値として配列に含まれる可能性があります。
- Set:
Set
はnull
とundefined
をそれぞれ別々のユニークな値として扱います。重複するnull
やundefined
は削除されます。 indexOf()
/includes()
: これらのメソッドもnull
とundefined
を正しく検索できます。- Map: Mapのキーとして
null
やundefined
を使うことも可能です。
したがって、これらの特殊な値が配列に含まれていても、SetやMap、あるいはindexOf
/includes
ベースの方法は通常通り動作し、null
や undefined
自体の重複も削除されます。
javascript
const mixedValues = [1, null, 2, undefined, 1, null, 3];
const uniqueMixedValues = [...new Set(mixedValues)];
console.log("\n--- null/undefinedを含む配列の重複削除 ---");
console.log(uniqueMixedValues); // [1, null, 2, undefined, 3]
11.4. 特定の条件を満たす要素だけを重複削除したい場合
例えば、「偶数だけを対象に重複を削除するが、奇数はそのまま残す」といった特殊な要件がある場合、単純な重複削除メソッドだけでは対応できません。
このような場合は、まず配列をいくつかのグループに分け、それぞれのグループに対して適切な処理を行い、最後に結合するなどのアプローチが必要です。
例: 偶数の重複だけを削除し、奇数はそのまま残す
“`javascript
const numbers = [1, 2, 3, 4, 2, 1, 5, 6, 3, 7, 8, 8, 9, 10, 4];
// 偶数と奇数に分ける
const evens = numbers.filter(num => num % 2 === 0);
const odds = numbers.filter(num => num % 2 !== 0);
// 偶数だけ重複を削除 (Setを使用)
const uniqueEvens = […new Set(evens)];
// 重複削除した偶数と、元の奇数を結合
const result = uniqueEvens.concat(odds);
console.log(“\n— 特定条件での重複削除 (偶数のみ対象) —“);
console.log(元の配列: ${numbers}
);
console.log(偶数: ${evens}
); // [2, 4, 2, 6, 8, 8, 10, 4]
console.log(奇数: ${odds}
); // [1, 3, 1, 5, 3, 7, 9]
console.log(重複削除した偶数: ${uniqueEvens}
); // [2, 4, 6, 8, 10]
console.log(最終結果: ${result}
); // [2, 4, 6, 8, 10, 1, 3, 1, 5, 3, 7, 9]
“`
このように、より複雑な要件の場合は、複数の配列メソッドやロジックを組み合わせる必要があります。
12. まとめ
この記事では、JavaScriptの配列から重複を削除するための様々な方法を学びました。それぞれの方法にはメリットとデメリットがあり、解決したい問題や扱うデータの種類、パフォーマンス要件によって最適な方法が異なります。
方法 | 特徴 | 重複判定基準 (プリミティブ) | 重複判定基準 (オブジェクト) | 計算量 (理論値) | 環境互換性 (ES6以降 Δ) | 初心者向け | オブジェクト配列推奨 |
---|---|---|---|---|---|---|---|
Set | 最もシンプル、高速 | 値 | 参照 | O(n) | Δ | ◎ | △ (参照ベース) |
filter + indexOf | 分かりやすいロジック | 値 | 参照 | O(n^2) | ◯ | ◯ | △ (参照ベース) |
filter + includes | 直感的だが非効率 (非推奨) | 値 | 参照 | O(n^2) ~ | △ | △ | × |
for/forEach + indexOf/includes | 手続き的で基本 | 値 | 参照 | O(n^2) | ◯ | ◯ | △ (参照ベース) |
Map | キー/値ペア、オブジェクト重複に強力 | 値 | キーによるカスタム定義 | O(n) | Δ | ◯ | ◎ (特定プロパティ) |
reduce + indexOf/includes | 関数型スタイル、非効率 | 値 | 参照 | O(n^2) | ◯ | △ | × |
Set + JSON.stringify | オブジェクト全体で判定、単純だが注意点多し | – | JSON文字列での値 | O(n * C) | Δ | ◯ | △ (制限あり) |
filter + findIndex/some | オブジェクト配列をプロパティで判定 | – | 特定プロパティ | O(n^2) | △ | ◯ | ◯ (ただし非効率) |
初心者の方へのオススメ
もしあなたがJavaScriptを始めたばかりであれば、まずは以下の2つの方法を理解し、使えるようにするのがオススメです。
- Setを使う方法 (
[...new Set(array)]
): プリミティブ型の配列の重複削除において、最もシンプルで効率的だからです。 - filter() + indexOf() を使う方法 (
array.filter((item, index) => array.indexOf(item) === index)
): Setが使えない古い環境への対応が必要な場合や、Setの仕組みがまだ難しいと感じる場合に、ロジックが比較的理解しやすいからです。ただし、パフォーマンスには注意が必要です。
状況に応じた最適な方法の選び方
- 最もシンプルかつ高速にプリミティブ型の重複を削除したい場合: Set
- 特定のプロパティを基準にオブジェクト配列の重複を削除したい場合: Map
- 古い環境でも動作させたい(かつSet/Mapを使えない)場合: filter + indexOf または for/forEach + indexOf/includes (ただしパフォーマンス注意)
- 配列が非常に小さい(数十件以下)場合: どの方法でも大差ないため、最も書き慣れている方法や、コードが簡潔になる方法 (Setや filter + indexOf) を選ぶと良いでしょう。
- パフォーマンスが非常に重要な場合 (特に大規模な配列): Set または Map
13. FAQ (よくある質問)
Q1: なぜSetが他の方法より速いのですか?
A1: Setは内部的に、要素の追加や検索を非常に高速に行えるデータ構造(ハッシュテーブルや類似の構造)を使用しています。これにより、配列の各要素を一度だけ処理すれば重複判定ができるため、計算量が配列サイズに比例する O(n) となります。一方、indexOf
やincludes
を使った O(n^2) の方法は、各要素に対して配列全体(またはその一部)を繰り返し検索するため、要素数が増えるとその検索コストが累積して処理時間が急増します。
Q2: オブジェクトの重複削除がプリミティブ型より難しいのはなぜですか?
A2: これはJavaScriptの等価性の判定方法に起因します。プリミティブ型は「値」が同じであれば等しいと判断されますが、オブジェクトは「参照」が同じであるかどうかがデフォルトの判定基準となります。内容が全く同じ新しいオブジェクトを作成しても、それはメモリ上の別の場所にあるため、異なるオブジェクト(異なる参照)とみなされます。重複を「内容が同じ」という意味で削除したい場合は、SetやindexOf
/includes
のデフォルトの判定では不十分であり、特定のプロパティを基準にするなど、別途判定ロジックを定義する必要があるため難しくなります。
Q3: 元の配列を直接変更(破壊的変更)する方法はありますか?
A3: 厳密には、splice()
メソッドを使ってループ中に重複要素を削除することで実現可能ですが、推奨しません。ループ中に要素を削除すると配列のインデックスがずれてしまい、コードが非常に複雑になりやすく、バグの温床となります。最も安全な破壊的変更の方法は、重複を削除した新しい配列を作成し、その後で元の配列変数にその新しい配列を再代入することです (originalArray = uniqueArray;
)。
Q4: 空の配列や null
/ undefined
が含まれる場合はどうなりますか?
A4: 記事中でも触れましたが、SetやMap、indexOf
/includes
ベースの方法は、null
やundefined
も通常の「値」として扱います。したがって、これらの値が含まれていても正しく重複が削除されます。空の配列 ([]
) や配列の中に空の配列が含まれる場合 ([[], []]
) も、Setはそれぞれをユニークなオブジェクト参照として扱うため、[[], []]
の重複は削除されません(Mapも同様)。[] === []
は false だからです。
Q5: 特定の条件を満たす要素だけを重複削除したい場合は?
A5: 例えば、「偶数だけ」や「特定のプロパティを持つオブジェクトだけ」など、限定された条件で重複削除を行いたい場合は、まずfilter()
メソッドを使って対象の要素だけを抽出し、その部分配列に対して重複削除を行い、最後に元の配列の残りの要素と結合するといった、複数の操作を組み合わせる必要があります。記事の「注意点と応用」セクションで簡単な例を紹介しました。
14. 付録:さらに深く学ぶために
14.1. JavaScriptのEquality (等価性) について
JavaScriptでは、値の等価性を判断する方法がいくつかあります。
- 厳密等価演算子 (
===
) と厳密不等価演算子 (!==
): 値と型の両方が一致する場合にtrue
を返します。オブジェクトの場合は、参照が一致するかどうかで判断します。Set、Map、indexOf
、includes
などは、内部的にこれに近い(SameValueZeroと呼ばれるより厳密な比較)等価性判定を行います。 - 抽象等価演算子 (
==
) と抽象不等価演算子 (!=
): 比較する前に、必要に応じてオペランドの型を変換(型強制)します。予期せぬ結果を招きやすいため、通常は厳密等価演算子 (===
) の使用が推奨されます。 Object.is()
:===
よりもさらに厳密な比較を行います。特にNaN === NaN
はfalse
ですが、Object.is(NaN, NaN)
はtrue
です。また、+0 === -0
はtrue
ですが、Object.is(+0, -0)
はfalse
です。Setは内部的にSameValueZero
というアルゴリズムを使用しており、これはObject.is()
に似ていますが、+0
と-0
を区別しない点で異なります (Set
では+0
と-0
は重複とみなされます)。
重複削除の際には、SetやindexOf
などのメソッドがどのような等価性判定を使っているかを理解しておくことが、特にオブジェクトを扱う上で重要です。
14.2. Big O記法 (計算量) について
記事中で何度か出てきた「O(n)」「O(n^2)」といった表現は、アルゴリズムの効率を示すための「Big O記法」と呼ばれるものです。
- O(n) (線形): 処理時間が配列のサイズ
n
に比例して増加します。要素数が2倍になれば、処理時間もおよそ2倍になります。SetやMapを使った方法がこれに該当します。効率が良いとされます。 - O(n^2) (二次): 処理時間が配列サイズの2乗に比例して増加します。要素数が2倍になれば、処理時間はおよそ4倍になります。要素数が増えるにつれて、処理時間が急激に増加します。filter + indexOf や forループ + includes などの方法がこれに該当します。大規模なデータには不向きです。
計算量は、アルゴリズムの理論的な効率を示すもので、実際の実行時間はハードウェアやJavaScriptエンジンの最適化など様々な要因に左右されます。しかし、配列サイズが大きくなったときの処理時間の傾向を理解する上で非常に役立ちます。SetやMapが大規模データ処理で推奨されるのは、その計算量が O(n) であるためです。
14.3. より高度な重複削除テクニック
この記事で紹介した方法以外にも、ライブラリを使用したり、特殊なデータ構造を組み合わせたりすることで、さらに効率的あるいは柔軟な重複削除を実現できる場合があります。しかし、ほとんどのWeb開発の現場では、SetやMap、あるいはfilter + indexOf といった組み込みの機能で十分に対応できます。まずは標準的な方法をしっかりマスターすることが重要です。
15. 終わりに
JavaScriptの配列の重複削除は、プログラミングにおいて頻繁に遭遇する課題です。この記事を通して、Set、Map、filter、indexOf、includes、reduceといった様々なメソッドを組み合わせることで、重複を削除できることを学びました。
どの方法を選ぶかは、扱うデータの種類(プリミティブかオブジェクトか)、配列のサイズ、求められるパフォーマンス、そしてターゲットとするJavaScript環境によって異なります。
初心者の方は、まずはSetを使ったシンプルで効率的な方法を試してみてください。そして、オブジェクト配列の重複削除が必要になったら、Mapの利用を検討しましょう。
この記事が、あなたのJavaScript学習の助けとなり、より自信を持って配列操作を行えるようになる一助となれば幸いです。
Happy Coding!