JavaScript数组去重终极指南:从基础到高级的多种方法(附面试题解析)
在前端开发中,数组去重是JavaScript中最常见的需求之一。本文将全面解析8种数组去重方法,包括基础实现、ES6新特性、性能优化等,并附上面试常见问题解析。
一、基础去重方法(ES5及以前)
-
双重循环法
最基础的去重方法,通过两层循环比较元素:function unique(arr) {const result = [];for (let i = 0; i < arr.length; i++) {let isDuplicate = false;for (let j = 0; j < result.length; j++) {if (arr[i] === result[j]) {isDuplicate = true;break;}}if (!isDuplicate) result.push(arr[i]);}return result; }// 示例 console.log(unique([1, 2, 2, 3])); // [1, 2, 3]
时间复杂度:O(n²)
适用场景:小型数组,兼容性要求高 -
indexOf优化法
使用indexOf
替代内层循环:function unique(arr) {const result = [];for (let i = 0; i < arr.length; i++) {if (result.indexOf(arr[i]) === -1) {result.push(arr[i]);}}return result; }
注意:
indexOf
内部也是循环,本质上还是O(n²)复杂度 -
排序相邻比较法
先排序后比较相邻元素:function unique(arr) {arr.sort();const result = [arr[0]];for (let i = 1; i < arr.length; i++) {if (arr[i] !== arr[i-1]) {result.push(arr[i]);}}return result; }
缺点:会改变原数组顺序,不适用于对象类型
二、ES6+ 高效去重方法
-
Set数据结构法(最常用)
ES6的Set自动处理唯一值:const unique = arr => [...new Set(arr)];// 示例 console.log(unique([1, 2, 2, '2', NaN, NaN])); // [1, 2, "2", NaN]
特点:
- 代码最简洁(9个字符最短实现)
- 支持
NaN
去重(NaN === NaN
为false,但Set能识别) - 时间复杂度和空间复杂度均为O(n)
-
Map数据结构法
利用Map的键唯一性:function unique(arr) {const map = new Map();return arr.filter(item => !map.has(item) && map.set(item, true)); }
优势:保留原始顺序,适合需要顺序的场景
-
filter + indexOf
利用索引位置判断:const unique = arr => arr.filter((item, index) => arr.indexOf(item) === index);
缺点:
indexOf
无法识别NaN
,时间复杂度O(n²) -
reduce累积法
使用reduce实现优雅去重:const unique = arr => arr.reduce((acc, cur) => acc.includes(cur) ? acc : [...acc, cur], [] );
三、特殊场景处理
-
对象数组去重
根据对象属性去重:function uniqueByKey(arr, key) {const map = new Map();return arr.filter(item => {const identifier = item[key];return !map.has(identifier) && map.set(identifier, true);}); }// 示例 const users = [{id: 1, name: 'Alice'},{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'} ]; console.log(uniqueByKey(users, 'id')); // 两个Alice对象只会保留一个
-
多维数组去重
递归处理嵌套数组:function deepUnique(arr) {const flatArr = arr.flat(Infinity);return [...new Set(flatArr)]; }// 示例 console.log(deepUnique([1, [2, [3, 2]], 4])); // [1, 2, 3, 4]
-
混合类型精确去重
区分数字1和字符串’1’:function strictUnique(arr) {const seen = new Map();return arr.filter(item => {const type = typeof item;const key = `${type}-${item}`;return !seen.has(key) && seen.set(key, true);}); }// 示例 console.log(strictUnique([1, '1', 2, 2])); // [1, "1", 2]
四、面试高频问题与陷阱解析
-
问题:Set去重有什么缺陷?
答:- 不区分
-0
和+0
(Set认为它们相等) - 对象内容相同但引用不同时无法去重
const obj = {a: 1}; console.log(unique([obj, obj, {a: 1}])); // [obj, {a:1}] → 两个不同对象
- 不区分
-
问题:如何实现O(n)时间复杂度的去重?
答:使用Set或Map数据结构,它们的查找操作是O(1)复杂度 -
陷阱:indexOf无法识别NaN
[NaN].indexOf(NaN) // -1 → 无法识别 [...new Set([NaN, NaN])] // [NaN] → 正确识别
-
问题:如何保留去重后的原始顺序?
答:使用Map或对象记录首次出现位置:function orderedUnique(arr) {return [...new Map(arr.map(item => [item, item])).values()]; }
-
性能大比拼(10,000个元素测试):
方法 耗时(ms) 可读性 适用场景 双重循环 1200 ★★☆ 小型数组 Set 2.5 ★★★★★ 现代浏览器 filter + indexOf 850 ★★★☆ 简单去重 Map 3.0 ★★★★☆ 需保留顺序的场景
五、去重方法总结表
方法 | 代码复杂度 | 时间复杂度 | 特殊类型支持 | 适用场景 |
---|---|---|---|---|
双重循环 | 高 | O(n²) | 所有类型 | 兼容性要求高的老项目 |
Set | 极低 | O(n) | ✅ NaN | 现代项目首选 |
Map | 中 | O(n) | ✅ 对象引用 | 需要保留顺序的场景 |
filter + indexOf | 低 | O(n²) | ❌ NaN | 简单数组去重 |
reduce | 中 | O(n²) | ❌ NaN | 函数式编程场景 |
排序相邻比较 | 中 | O(n log n) | ❌ 混合类型 | 纯数字/字符串数组 |
对象属性去重 | 高 | O(n) | ✅ 按指定属性去重 | 对象数组去重 |
六、最佳实践与使用建议
-
现代项目首选
// 99%场景使用Set足够 const unique = arr => [...new Set(arr)];
-
需要兼容IE时
// 使用对象+类型标记的polyfill function legacyUnique(arr) {const seen = {};return arr.filter(item => {const key = typeof item + JSON.stringify(item);return seen.hasOwnProperty(key) ? false : (seen[key] = true);}); }
-
大数组优化
对于超过10,000个元素的数组:function bigArrayUnique(arr) {const set = new Set();const result = [];for (let i = 0; i < arr.length; i++) {if (!set.has(arr[i])) {set.add(arr[i]);result.push(arr[i]);}}return result; }
-
TypeScript强化版
function typedUnique<T>(arr: T[]): T[] {return Array.from(new Set(arr)); }// 对象数组按key去重 function uniqueByKey<T>(arr: T[], key: keyof T): T[] {const map = new Map<any, T>();arr.forEach(item => {const identifier = item[key];if (!map.has(identifier)) {map.set(identifier, item);}});return Array.from(map.values()); }
掌握这些去重方法,不仅能轻松应对面试,更能根据实际场景选择最优方案。记住:当简单方案(如Set)能满足需求时,不要过度设计。建议在项目中统一封装去重工具函数。