JS 数组或数组对象去重的9种方法
一、背景与问题
在前端开发中,处理数据去重是常见的需求。无论是用户输入的表单数据、API返回的列表数据,还是本地存储的缓存数据,重复数据都可能影响程序的性能和用户体验。对于数组去重,核心问题是:如何高效地识别并移除重复元素。
JS数组的重复元素可以是基本类型(如数字、字符串),也可以是对象(如包含id、name等属性的对象)。不同类型的去重需要不同的处理方式,而常见的误区包括:
- 直接使用
Set处理对象数组 - 忽略类型转换导致的潜在问题
- 对大数据量使用低效算法
本文将深入分析9种数组去重方法,涵盖不同场景下的实现原理、性能对比和实际应用建议。
二、基本原理
1. 哈希表原理(Set/Map)
- 哈希表通过键值对存储数据,查找时间复杂度为O(1)
- 适用于处理基本类型和简单对象
- 对象需要通过唯一标识(如id)进行映射
2. 线性查找原理(includes/filter)
- 遍历数组时逐个检查元素是否已存在
- 时间复杂度O(n²),适合小数据量
- 对象需要深度比较(JSON.stringify)
3. 排序去重原理(sort+splice)
- 先排序再遍历,利用相同元素相邻的特性
- 时间复杂度O(n log n),适合需要排序的场景
- 对象需要先处理为可比较的键
4. 递归去重原理
- 通过递归分解数组,逐层处理子数组
- 适合处理嵌套结构的复杂数据
三、环境准备
# 安装开发依赖(如需)
npm install --save-dev eslint// 示例环境
const assert = require('assert');四、核心实现
方法1:Set去重(适用于基本类型)
function uniqueSet(arr) {
return Array.from(new Set(arr));
}原理分析:
- Set构造函数会自动过滤重复元素
Array.from将Set转换为数组- 适用于字符串、数字等基本类型
局限性:
- 无法处理对象数组(如
[{id:1}, {id:1}])
方法2:filter+includes去重(适用于小数据量)
function uniqueFilter(arr) {
return arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
}原理分析:
indexOf方法查找元素索引- 通过索引判断是否是首次出现
- 时间复杂度O(n²)
改进版:
function uniqueFilterOpt(arr) {
const seen = new Set();
return arr.filter(item => {
const key = typeof item === 'object' ? JSON.stringify(item) : item;
return seen.has(key) ? false : seen.add(key);
});
}注意事项:
- 对象需通过
JSON.stringify转换为字符串 - 会改变原数组顺序
方法3:sort+splice去重(适用于需要排序的场景)
function uniqueSort(arr) {
arr.sort();
for (let i = 0; i < arr.length; i++) {
if (arr[i] === arr[i+1]) {
arr.splice(i, 1);
i--;
}
}
return arr;
}原理分析:
- 先排序使相同元素相邻
- 通过
splice移除重复项 - 时间复杂度O(n log n)
适用场景:
- 需要保持有序输出
- 可处理简单类型数组
方法4:reduce+includes去重(适用于对象数组)
function uniqueReduce(arr) {
return arr.reduce((acc, item) => {
const key = typeof item === 'object' ? JSON.stringify(item) : item;
return acc.includes(key) ? acc : [...acc, key];
}, []);
}原理分析:
- 使用
reduce累积唯一键 - 通过
includes判断是否已存在 - 适用于对象数组的键值去重
性能优化:
function uniqueReduceOpt(arr) {
return arr.reduce((acc, item) => {
const key = typeof item === 'object' ? JSON.stringify(item) : item;
const exists = acc.some(k => k === key);
return exists ? acc : [...acc, key];
}, []);
}改进点:
- 使用
some代替includes提高性能 - 避免不必要的数组复制
五、完整案例
场景:用户输入数据去重处理
<!-- 前端页面 -->
<input type="text" id="input" placeholder="输入内容">
<button onclick="process()">处理</button>
<pre id="output"></pre>
<script>
function process() {
const input = document.getElementById('input').value;
const arr = input.split(',').map(s => s.trim());
// 使用Set去重
const unique = uniqueSet(arr);
// 显示结果
document.getElementById('output').textContent = JSON.stringify(unique, null, 2);
}
</script>// 后端处理(Node.js)
app.post('/process', (req, res) => {
const { data } = req.body;
const unique = uniqueSet(data.split(',').map(s => s.trim()));
res.json({ result: unique });
});性能测试:
// 压力测试代码
const bigArray = Array(100000).fill(null).map((_, i) => i % 100);
console.time('去重时间');
console.log(uniqueSet(bigArray));
console.timeEnd('去重时间');六、源码解析
以uniqueReduceOpt方法为例:
function uniqueReduceOpt(arr) {
return arr.reduce((acc, item) => {
const key = typeof item === 'object' ? JSON.stringify(item) : item;
const exists = acc.some(k => k === key);
return exists ? acc : [...acc, key];
}, []);
}关键点解析:
reduce初始值为一个空数组some方法比includes效率更高(O(n) vs O(n²))- 使用展开运算符避免修改原数组
- 对象通过
JSON.stringify转换为可比较的字符串
性能对比:
| 方法 | 时间复杂度 | 适用场景 |
|---|---|---|
| Set | O(n) | 基本类型数组 |
| filter+includes | O(n²) | 小数据量 |
| sort+splice | O(n log n) | 需要排序 |
| reduce+includes | O(n²) | 对象数组 |
| reduce+some | O(n²) | 优化版对象数组 |
七、进阶使用
对象数组去重优化
function uniqueObjects(arr, key) {
return arr.reduce((acc, item) => {
const id = item[key];
if (!acc.includes(id)) {
acc.push(item);
}
return acc;
}, []);
}使用示例:
const data = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' }
];
const unique = uniqueObjects(data, 'id');多字段去重
function uniqueMultiKey(arr, keys) {
return arr.reduce((acc, item) => {
const key = keys.map(k => item[k]).join('|');
if (!acc.includes(key)) {
acc.push(item);
}
return acc;
}, []);
}使用示例:
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 1, name: 'Alice', age: 25 }
];
const unique = uniqueMultiKey(data, ['id', 'age']);八、性能与工程实践
性能优化策略
- 数据预处理:将对象数组转换为唯一标识的数组
- 使用Map:替代Set处理对象数组
- 并行处理:使用Web Workers处理大数据量
- 记忆化:缓存已处理过的数据
异常处理方案
function safeUnique(arr) {
try {
return uniqueSet(arr);
} catch (e) {
console.error('去重失败:', e);
return arr;
}
}安全性考虑
- 避免使用
eval或new Function处理用户输入 - 对JSON.stringify的结果进行校验
- 使用
JSON.parse时添加try/catch
九、常见问题与踩坑
常见错误
| 错误 | 原因 | 解决方案 |
|---|---|---|
| 方法1 | 无法处理对象数组 | 使用JSON.stringify转换 |
| 方法2 | 混淆对象引用 | 使用深度比较 |
| 方法3 | 排序不彻底 | 确保排序算法的稳定性 |
| 方法4 | 忽略类型转换 | 添加类型校验逻辑 |
典型错误示例
// 错误:直接使用Set处理对象数组
const data = [{id:1}, {id:1}];
const unique = Array.from(new Set(data)); // 不会去重正确实现
// 正确:使用Map处理对象数组
function uniqueObjects(arr) {
return Array.from(new Map(arr.map(item => [JSON.stringify(item), item]))).map(([k, v]) => v);
}十、最佳实践
推荐方案
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 基本类型数组 | Set | 简洁高效 |
| 对象数组 | Map | 可控性强 |
| 需要排序 | sort+splice | 保持有序输出 |
| 大数据量 | 哈希表 | 时间复杂度O(n) |
| 多字段去重 | 多字段hash | 灵活性强 |
实施建议
- 先做类型校验:确保输入数据符合预期
- 考虑内存占用:处理大数据时使用分页
- 添加日志记录:记录去重前后数据变化
- 使用性能分析工具:如Chrome DevTools的Performance面板
十一、总结
JS数组去重是一个看似简单但涉及多维度技术的难题。从基本类型的Set去重到复杂对象数组的处理,需要根据具体场景选择合适的方法。本文通过9种方法的对比分析,深入探讨了不同实现的原理、适用场景和性能特点。
在实际开发中,建议优先使用Set和Map处理基本类型和对象数组,对于特殊需求可采用排序、递归等方法。同时,要特别注意数据类型转换、性能优化和安全风险,确保在不同场景下都能稳定运行。
记住:去重不是简单的数据过滤,而是对数据结构和算法的深刻理解。掌握这些技术,将帮助你在处理复杂数据时更加得心应手。