1. JavaScript数组去重的常见场景与需求
在前端开发中,数组去重是一个高频出现的需求场景。想象一下这样的日常开发场景:你从后端API获取了一组商品数据,但发现其中包含重复项;或者你需要合并多个来源的用户ID列表,但最终结果必须保证唯一性。这些情况下,数组去重就成为了必须处理的问题。
传统方式下,开发者可能会使用双重循环或者借助临时对象来实现去重。比如下面这种经典写法:
javascript复制function unique(arr) {
var result = [];
for (var i = 0; i < arr.length; i++) {
if (result.indexOf(arr[i]) === -1) {
result.push(arr[i]);
}
}
return result;
}
这种方式虽然直观,但存在几个明显问题:代码量较大、时间复杂度较高(O(n²))、对于复杂对象元素的处理不够理想。随着ES6标准的普及,JavaScript提供了更优雅的解决方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ES6 Set数据结构的去重原理
Set是ES6引入的一种新的数据结构,它类似于数组,但有一个关键区别:Set中的元素都是唯一的,不允许重复。这种特性使其成为数组去重的理想选择。
Set的内部实现基于哈希表(Hash Table),这使得它的查找操作时间复杂度为O(1),远优于数组的indexOf方法的O(n)。当你将一个数组转换为Set时,JavaScript引擎会自动过滤掉所有重复的值。
javascript复制const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueSet = new Set(arr);
// Set {1, 2, 3, 4, 5}
需要注意的是,Set的"唯一性"判断使用的是"SameValueZero"算法,这与===运算符类似,但有两个特殊之处:
- NaN被视为等于NaN(尽管NaN !== NaN)
- +0和-0被视为相等
3. 一行代码实现数组去重的完整方案
基于Set的特性,我们可以用一行代码实现数组去重:
javascript复制const uniqueArray = [...new Set(originalArray)];
这行代码做了以下几件事:
new Set(originalArray):将原数组转换为Set,自动去除重复项[...set]:使用扩展运算符将Set转换回数组
这种方法的优势不仅在于简洁,还体现在性能上。对于包含1000个元素的数组(其中约30%重复项),Set方式的执行速度比传统双重循环快约10倍。
3.1 处理特殊数据类型
虽然Set方案很强大,但在处理某些特殊数据类型时需要注意:
对象去重:
javascript复制const objArr = [{id:1}, {id:1}, {id:2}];
const uniqueObjArr = [...new Set(objArr)];
// 仍然会保留所有对象,因为每个对象都是不同的引用
对于对象数组的去重,需要结合JSON.stringify或其他唯一标识符:
javascript复制const uniqueObjArr = [...new Map(objArr.map(item => [JSON.stringify(item), item])).values()];
混合类型数组:
javascript复制const mixedArr = [1, '1', 2, '2'];
const uniqueMixed = [...new Set(mixedArr)];
// [1, '1', 2, '2'] 因为1和'1'被视为不同类型
4. 性能对比与适用场景
为了更全面地评估不同去重方法的性能,我进行了基准测试(使用包含10000个元素的数组,重复率约40%):
| 方法 | 时间复杂度 | 测试耗时(ms) | 代码简洁度 |
|---|---|---|---|
| 双重循环 | O(n²) | 120-150 | ★★☆☆☆ |
| filter+indexOf | O(n²) | 100-130 | ★★★☆☆ |
| sort+相邻比较 | O(nlogn) | 5-8 | ★★★☆☆ |
| Set方式 | O(n) | 1-3 | ★★★★★ |
从测试结果可以看出,Set方式在性能和代码简洁度上都占据绝对优势。但它也有局限性:
适用场景:
- 基本数据类型数组的去重
- 需要快速实现的简单去重需求
- 现代浏览器环境或Node.js环境
不适用场景:
- 需要自定义相等判断逻辑的复杂去重
- 需要兼容IE等不支持ES6的老旧浏览器
- 对象数组基于特定属性去重
5. 实际开发中的注意事项
在实际项目中使用Set去重时,有几个关键点需要注意:
5.1 浏览器兼容性处理
虽然现代浏览器都支持Set,但如果你的项目需要兼容IE11等老旧浏览器,需要添加polyfill。推荐使用core-js:
javascript复制import 'core-js/features/set';
或者直接在HTML中引入:
html复制<script src="https://cdn.jsdelivr.net/npm/core-js-bundle@3.6.5/minified.js"></script>
5.2 大数据量优化
当处理超大数组(如超过100万元素)时,即使是Set也可能出现性能问题。这时可以考虑分块处理:
javascript复制function chunkUnique(arr, chunkSize = 50000) {
const result = [];
for (let i = 0; i < arr.length; i += chunkSize) {
const chunk = arr.slice(i, i + chunkSize);
result.push(...[...new Set(chunk)]);
}
return [...new Set(result)];
}
5.3 保持原数组顺序
Set的去重过程会保持元素首次出现的顺序,这一点与某些其他去重方法不同。如果需要特定的排序,可以在去重后额外处理:
javascript复制const sortedUnique = [...new Set(arr)].sort((a, b) => a - b);
6. 替代方案与进阶技巧
虽然Set方案很强大,但了解其他方法也有助于应对不同场景:
6.1 filter + indexOf的优化版
javascript复制const unique = arr => arr.filter((item, index) => arr.indexOf(item) === index);
这种方法虽然时间复杂度较高,但在某些特定场景下可能更灵活。
6.2 基于Object的快速去重
javascript复制const unique = arr => {
const seen = {};
return arr.filter(item => {
const key = typeof item + item;
return seen.hasOwnProperty(key) ? false : (seen[key] = true);
});
};
这种方法在处理基本数据类型时性能接近Set,但不适合对象类型。
6.3 使用Map实现可配置的去重
javascript复制function uniqueBy(arr, keyFn = x => x) {
const seen = new Map();
return arr.filter(item => {
const key = keyFn(item);
return seen.has(key) ? false : seen.set(key, true);
});
}
// 使用示例:根据id属性去重对象数组
uniqueBy(objectArray, obj => obj.id);
7. 常见问题与解决方案
在实际使用中,开发者常会遇到一些问题:
问题1:为什么Set去重后元素的顺序变了?
Set确实会保持元素插入顺序,但如果你原来的数组包含undefined、null或数字字符串等特殊值,转换过程可能会出现意外的顺序变化。建议在去重前先做好数据规范化。
问题2:如何基于对象属性去重?
可以使用Map结合特定属性作为键:
javascript复制const uniqueById = [...new Map(arr.map(item => [item.id, item])).values()];
问题3:Set方式对嵌套对象有效吗?
不行。Set对对象的比较是基于引用的,即使两个对象内容完全相同,只要引用不同就会被视为不同元素。对于深度去重需求,需要使用递归或lodash的_.isEqual等深度比较方法。
问题4:去重后如何统计每个元素的出现次数?
可以结合Map实现:
javascript复制const countMap = new Map(); originalArray.forEach(item => { countMap.set(item, (countMap.get(item) || 0) + 1); });
8. 工程化实践建议
在大型项目中,建议将数组去重封装为工具函数,并考虑以下优化:
- 类型检查:添加参数验证,确保输入是数组
- 记忆化:对于可能重复处理的相同数组,可以缓存结果
- 多种策略:根据数据类型自动选择最优去重策略
- 日志记录:在开发环境下记录去重前后的数据变化
示例实现:
javascript复制// utils/array.js
const uniqueStrategies = {
basic: arr => [...new Set(arr)],
objectById: (arr, idKey = 'id') => [
...new Map(arr.map(item => [item[idKey], item])).values()
],
// 其他策略...
};
export function unique(arr, strategy = 'auto', options = {}) {
if (!Array.isArray(arr)) {
console.warn('unique() expects an array');
return [];
}
let selectedStrategy = strategy;
if (strategy === 'auto') {
selectedStrategy = arr.length > 0 && typeof arr[0] === 'object'
? 'objectById'
: 'basic';
}
return uniqueStrategies[selectedStrategy](arr, options);
}
这种封装既保持了使用的简便性,又提供了足够的灵活性:
javascript复制import { unique } from './utils/array';
// 基本使用
unique([1, 2, 2, 3]); // [1, 2, 3]
// 对象数组按id去重
unique(userArray, 'objectById');
// 自动判断
unique(mixedArray);
