iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

JavaScript配列の重複削除でつまづかない!Set・Mapを徹底比較(Lodash・Ramdaも少し紹介)

こんにちは!株式会社iimonでエンジニアをしている、Javascript勉強中の「まるお」です。

本記事はアドベントカレンダー20日目の記事になります!

📚 本記事の内容

  • filterSetMapを使った重複削除の方法
  • オブジェクト配列の重複削除の実装方法
  • パフォーマンス比較と便利なライブラリの紹介

はじめに

業務で配列の重複削除をする必要があり、SetやMapを使って実装していたところ、使い方を誤り「なんで重複が削除されていないの!?」と混乱することに…。

今回、業務でつまずいた経験をもとに、JavaScriptでの効率的な重複削除の方法をまとめました。

実務で使える知識を学びたい方にとって、少しでも役立つ情報になれば嬉しいです!


🔍 1. 重複削除したい!配列メソッドの課題…

まずは基本の配列メソッド filterindexOf を使って重複削除してみます。

🚀 filter()indexOf() を使った重複削除

const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = numbers.filter((item, index, array) =>
  array.indexOf(item) === index
);

console.log(uniqueNumbers); // [1, 2, 3, 4, 5]

❗️ 問題点

  1. indexOf() を毎回呼び出すため、データ量が多いと非効率
  2. オブジェクトのような複雑なデータには不向き。

🛠 2. Set による重複削除

✅ Setの特徴

Set は、一意の値のみを保持するデータ構造です。重複した値は自動的に排除されます。

✨ シンプルな重複削除の例

const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = [...new Set(numbers)];

console.log(uniqueNumbers); // [1, 2, 3, 4, 5]

⚠️ 注意点: オブジェクトや配列の扱い

Set値の一意性===(厳密等価演算子)で判断します。

そのため、同じ内容のオブジェクトでも参照が異なると重複と見なされません。

まさにここが私がつまずいた箇所です。

const set = new Set();
set.add({ id: 1 });
set.add({ id: 1 }); // 別のオブジェクトとして扱われる

console.log(set.size); // 2

実務で重複削除したかったコード

やりたいこと: companystoreが同じ場合は重複削除したい

このコードを
const accounts = [{ company: "iimon", store: "東京" },
               { company: "iimon", store: "東京" },
               { company: "iimon", store: "大阪" },
               { company: "iimon", store: "福岡" }];
               
こうしたい
const accounts = [{ company: "iimon", store: "東京" },
               { company: "iimon", store: "大阪" },
                { company: "iimon", store: "福岡" }];

Setでそのままオブジェクトをユニークに出来ると思ったのですが、Set では参照が異なるオブジェクトとして扱われ、ユニークにできませんでした。

const uniqueAccountsSet = Array.from(new Set(accounts));
console.log(uniqueAccountsSet) 
/*
[
  { company: "iimon", store: "東京" },
  { company: "iimon", store: "東京" },
  { company: "iimon", store: "大阪" },
  { company: "iimon", store: "福岡" }
]
*/

💡 3. Map による重複削除

次に、Mapでキーを重複させてユニークにする方法に変更しました。

✨ 実務で重複削除したかったコード

const accounts = [
  { company: "iimon", store: "東京" },
  { company: "iimon", store: "東京" },
  { company: "iimon", store: "大阪" },
  { company: "iimon", store: "福岡" }
];

const uniqueAccounts = Array.from(
  new Map(accounts.map(account => [`${account.company}-${account.store}`, account])).values()
);

console.log(uniqueAccounts);
/*
[
  { company: "iimon", store: "東京" },
  { company: "iimon", store: "大阪" },
  { company: "iimon", store: "福岡" }
]
*/

このように、Map を使用することで、キー${account.company}-${account.store}を基準にオブジェクトの重複削除が実現できました。

キーが重複した場合の挙動

ちなみに、、Map では同じキーが追加されると新しい値で上書きされます。

const map = new Map();
map.set('key', 'value1');
map.set('key', 'value2');

console.log(map.get('key')); // 'value2'

🚀 4. 大規模データでのパフォーマンス比較

ここではプリミティブ値に限定して、filter、Set、Map の重複削除のパフォーマンスを比較してみました。

const largeArray = Array.from({ length: 1000000 }, () => Math.floor(Math.random() * 10000));

// filter と indexOf
console.time('Filter');
const uniqueWithFilter = largeArray.filter((item, index, self) => self.indexOf(item) === index);
console.timeEnd('Filter');

// Set
console.time('Set');
const uniqueWithSet = [...new Set(largeArray)];
console.timeEnd('Set');

// Map
console.time('Map');
const uniqueWithMap = Array.from(
  new Map(largeArray.map(item => [item, item])).values()
);
console.timeEnd('Map');

📈 結果の比較

方法 処理時間 (例)
Set 16ms
Map 39ms
filter + indexOf 719ms

結論:

  • Set は最速で重複削除が可能!
  • Map はオブジェクトなど柔軟な条件の重複削除に最適。

🔧 5. ライブラリを使った重複削除

SetMap の代わりに、ユーティリティライブラリ(LodashとRamda)を使った重複削除の方法もあるみたいなので、最後に少しだけ紹介します!

  1. Lodash: JavaScript定番の便利ライブラリ
  2. Ramda: 関数型プログラミングをサポート

Lodash やRamdaを使った動作確認は、CDNでLodashやRamdaを読み込むか、npmでビルド時にインストールするか、CodePen等を利用すると便利です!


✨ 5.1 LodashuniqBy でオブジェクトの重複削除

LodashuniqBy を使えば、指定したキーや条件でオブジェクト配列の重複を削除できます。

コード例:オブジェクト配列の重複削除

const accounts = [
  { company: "iimon", store: "東京" },
  { company: "iimon", store: "東京" },
  { company: "iimon", store: "大阪" },
  { company: "iimon", store: "福岡" }
];

const uniqueAccounts = _.uniqBy(accounts, account => `${account.company}-${account.store}`);

console.log(uniqueAccounts);
// [{ company: "iimon", store: "東京" }, { company: "iimon", store: "大阪" }, { company: "iimon", store: "福岡" }]

💡 ポイント

  • 第2引数で 重複の基準${account.company}-${account.store})を指定するだけで、簡単に重複削除が可能!

✨ 5.2 RamdauniquniqWithでオブジェクトの重複削除

  • Ramdauniqは、R.equals を使って等価性を判定します。 Set のような ===(厳密等価演算子)ではなく、値の内容を基にして判定するので、 オブジェクトや配列の場合でも、内容が同じであれば等しいとみなします。
  • 一方でRamdauniqWithは、カスタム関数を指定して柔軟な条件で比較することが可能です。

コード例:オブジェクト配列の重複削除(uniq)

const accounts = [
  { company: "iimon", store: "東京" },
  { company: "iimon", store: "東京" },
  { company: "iimon", store: "大阪" },
  { company: "iimon", store: "福岡" }
];

const uniqueAccounts = R.uniq(accounts);

console.log(uniqueAccounts);
// [{ company: "iimon", store: "東京" }, { company: "iimon", store: "大阪" }, { company: "iimon", store: "福岡" }]

コード例:オブジェクト配列の重複削除(uniqWith)

const accounts = [
  { company: "iimon", store: "東京" },
  { company: "iimon", store: "東京" },
  { company: "iimon", store: "大阪" },
  { company: "iimon", store: "福岡" }
];

const uniqueAccounts = R.uniqWith(
  (a, b) => a.company === b.company && a.store === b.store,
  accounts
);

console.log(uniqueAccounts);
// [{ company: "iimon", store: "東京" }, { company: "iimon", store: "大阪" }, { company: "iimon", store: "福岡" }]

💡 ポイント

  • uniq は、キーを指定せずともシンプルに重複削除できる!
    • 型が混在した配列(例:文字列と数値など)では、期待通りに動作しないので注意が必要。
  • uniqWith は、カスタム条件を指定して柔軟な重複削除が可能。

📊 パフォーマンス比較: Set・Map vs Lodash vs Ramda

プリミティブ値に限定して、Set・Map・Lodash・Ramda の重複削除のパフォーマンスを比較してみました。

const largeArray = Array.from({ length: 1000000 }, () => Math.floor(Math.random() * 10000));

// Set
console.time('Set');
const uniqueWithSet = [...new Set(largeArray)];
console.timeEnd('Set');

// Map
console.time('Map');
const uniqueWithMap = Array.from(
  new Map(largeArray.map(item => [item, item])).values()
);
console.timeEnd('Map');

// Lodash
console.time('Lodash');
const uniqueWithLodash = _.uniqBy(largeArray);
console.timeEnd('Lodash');

// Ramda(uniq)
console.time('Ramda uniq');
const uniqueWithRamdaUniq = R.uniq(largeArray);
console.timeEnd('Ramda uniq');

// Ramda(uniqWith)
console.time('Ramda uniqWith');
const uniqueWithRamdaUniqWith = R.uniqWith((a,b) => a === b)(largeArray);
console.timeEnd('Ramda uniqWith');

📈 結果の比較

出力結果は環境やデータ量によって異なりますが、、今回のパフォーマンス比較では以下の結果が得られました。

方法 処理時間 特徴
⚡️ Lodash 7.20 ms 状況によっては最速で重複削除可能。
🔧 Set 14.95 ms ライブラリに依存せず、高速に重複削除できる。
🚀 Ramda uniq 19.99 ms ===では対応できない等価性判定をシンプルなコードで実装可能。
🔑 Map 38.54 ms 複雑な条件やキー管理を含む重複削除向け。パフォーマンスは他より劣る。
🚀 Ramda uniqWith 1500.50 ms 柔軟な重複削除が可能な分、パフォーマンスは他より大幅に劣る。

6. まとめ

今回、JavaScriptにおける配列の重複削除について、いろいろなアプローチ法があることを学びました。

  • filter と indexOf
    • 小規模データには使えますが、パフォーマンスが低く、複雑なデータ構造には不向き。
  • Set
    • 最速かつシンプルに重複削除ができますが、オブジェクトや配列の参照問題には注意が必要。
  • Map
    • キーに基づく重複削除が可能で、オブジェクトの重複削除には最適。
    • ただし、キーにオブジェクトを設定することもでき、この場合単純なオブジェクトのキーとして扱えないため注意が必要(例: { [{"a": 1}]: 2 } )。
  • Lodash と Ramda
    • ライブラリを活用することで、オブジェクトや配列でも簡潔に重複削除が実装可能。

そしてそれぞれに一長一短があるので、実装したい内容によって使用するアプローチ法を変えることで、データ処理がもっと効率的になると感じました。

Lodash やRamdaはまだ触ったばかりなので、より実務的な課題に対応できるように、もう少し踏み込んで学習してみたいと思います。

今後も実務で活かせるJavaScriptの知識をどんどん吸収して発信していきたいと思います!

おわりに

明日のアドベントカレンダー担当は、なんと‼️明日がお誕生日🎊㊗️🎉

我らのエンジニアマネージャー「まつだ」さんです‼️‼️

どんな記事を投稿するのか今から楽しみです!!

最後になりますが、現在弊社ではエンジニアを募集しています!

この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!

 iimon採用サイト / Wantedly / Green

最後まで読んでいただきありがとうございました!!!

参考

Set - JavaScript | MDNdeveloper.mozilla.org

Map - JavaScript | MDNdeveloper.mozilla.org

Lodashlodash.com

Map と Setja.javascript.info

Lodash _.keyBy() Method - GeeksforGeekswww.geeksforgeeks.org

Lodash _.uniqBy() Method - GeeksforGeekswww.geeksforgeeks.org

Ramda Documentationramdajs.com

Ramda Documentationramdajs.com

Ramda おすすめ機能ベスト10 #JavaScript - Qiitaqiita.com

JavaScriptで配列から重複を削除する方法10選 – Japanシーモアjp-seemore.com

JavaScriptで配列の重複を削除する方法 #JavaScript - Qiitaqiita.com