js 数组去重(手把手讲解)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目:《Spring AI 项目实战》 正在持续爆肝中,基于 Spring AI + Spring Boot 3.x + JDK 21..., 点击查看 ;
- 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观
在 JavaScript 开发中,数组去重是一个基础却重要的操作。无论是处理用户输入的数据、过滤重复的 API 响应,还是优化数据展示逻辑,开发者都需要掌握数组去重的核心方法。对于编程初学者而言,这一问题既简单又容易陷入误区;而对中级开发者来说,如何选择高效且可维护的解决方案则是关键。本文将从零开始,通过通俗易懂的比喻、代码示例和性能分析,系统讲解多种 JavaScript 数组去重方法,并结合实际案例帮助读者深入理解其原理与适用场景。
一、基础方法:循环遍历与条件判断
1.1 数组去重的核心逻辑
数组去重的本质是“保留唯一值,移除重复项”。想象你正在整理书架,每本书都有一个唯一的 ISBN 号码。当你发现两本相同 ISBN 的书时,只需保留一本即可。在 JavaScript 中,数组去重的逻辑与此类似:遍历数组中的每个元素,并确保每个元素只出现一次。
1.2 传统 for 循环实现
最基础的方法是通过 for
循环结合条件判断实现:
function uniqueArray(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (!result.includes(arr[i])) {
result.push(arr[i]);
}
}
return result;
}
// 示例:
const arr = [1, 2, 2, 3, 4, 4, 5];
console.log(uniqueArray(arr)); // [1, 2, 3, 4, 5]
原理说明:
- 创建一个空数组
result
作为结果容器。 - 遍历原始数组的每个元素,若
result
中尚未包含该元素,则将其加入result
。 - 最终返回去重后的数组。
1.3 问题与改进
上述方法存在两个潜在问题:
- 时间复杂度高:
includes()
方法本身需要遍历result
数组,导致整体时间复杂度为 O(n²),当处理大数据量时性能较差。 - 无法处理非原始值:例如对象或数组元素的去重,因为
includes()
会基于严格相等(===
)比较,而对象引用地址不同会被视为不同元素。
改进方案:
// 使用对象作为哈希表优化性能
function uniqueArrayOptimized(arr) {
const seen = {};
const result = [];
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if (!seen[item]) {
seen[item] = true;
result.push(item);
}
}
return result;
}
通过对象 seen
存储已出现的元素,利用对象键的唯一性特性,将时间复杂度降低到 O(n)。
二、进阶方法:ES6 新特性与高阶函数
2.1 Set 数据结构
ES6 引入的 Set
对象天生具备去重功能,其核心特性是“值的集合,且元素唯一”。
const arr = [1, 2, 2, 3, 4, 4, 5];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4, 5]
优势:
- 代码简洁,一行解决去重问题。
- 时间复杂度为 O(n),性能高效。
注意事项:
Set
仅适用于可遍历的值(如数字、字符串等原始值)。若需处理对象数组,需结合其他方法。- 需注意浏览器兼容性,确保目标环境支持 ES6。
2.2 Array.prototype.filter() 与 includes() 结合
const unique = arr => arr.filter((item, index) => arr.indexOf(item) === index);
或使用 includes
:
const unique = arr => arr.filter((item, index) => !arr.includes(item, index + 1));
原理:
filter()
方法遍历数组,对每个元素判断其首次出现的位置是否为当前索引。- 第二种方法通过
includes
检查当前元素是否在后续位置重复出现。
性能对比:
| 方法 | 时间复杂度 | 代码简洁性 | 兼容性 |
|---------------------|------------|------------|-------------|
| for 循环 + 哈希表 | O(n) | 中等 | 全平台 |
| Set | O(n) | 高 | 需 ES6 |
| filter + indexOf | O(n²) | 中等 | 全平台 |
| filter + includes | O(n²) | 中等 | 全平台 |
三、高级技巧:对象与 Map 的应用
3.1 对象键存储法
对于非 ES6 环境或需兼容旧版本的场景,可通过对象键的唯一性实现去重:
function uniqueWithObject(arr) {
const temp = {};
return arr.filter(item => {
const key = typeof item === 'object' ? JSON.stringify(item) : item;
return temp[key] ? false : (temp[key] = true);
});
}
// 示例:处理对象数组
const arr = [
{ id: 1 },
{ id: 1 },
{ id: 2 }
];
console.log(uniqueWithObject(arr)); // 只保留第一个和第三个对象
关键点:
- 将对象转为 JSON 字符串作为键,解决对象引用地址不同的问题。
3.2 Map 结构优化
Map
可以存储键值对,并利用键的唯一性实现去重:
function uniqueWithMap(arr) {
const map = new Map();
return arr.filter(item => {
const key = JSON.stringify(item);
if (!map.has(key)) {
map.set(key, true);
return true;
}
return false;
});
}
此方法在处理复杂对象时更灵活,但需要 ES6 支持。
四、算法实现:递归与分治策略
4.1 递归法去重
通过递归不断缩小问题规模,逐步去除重复项:
function uniqueRecursive(arr) {
if (arr.length <= 1) return arr;
const [first, ...rest] = arr;
return [
first,
...uniqueRecursive(rest.filter(item => item !== first))
];
}
缺点:
- 时间复杂度为 O(n²),递归深度过大会导致栈溢出。
4.2 分治法去重
将数组拆分为子数组,分别去重后再合并:
function divideAndConquer(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = divideAndConquer(arr.slice(0, mid));
const right = divideAndConquer(arr.slice(mid));
const combined = [...left, ...right];
// 合并后去重
return [...new Set(combined)];
}
此方法结合了分治策略与 Set 去重,但实际性能可能不如直接使用 Set。
五、性能优化与场景选择
5.1 时间复杂度对比
方法 | 时间复杂度 | 适用场景 |
---|---|---|
for 循环 + 哈希表 | O(n) | 大数据量,兼容性要求高 |
Set 转数组 | O(n) | 现代浏览器,简洁代码优先 |
filter + indexOf | O(n²) | 小数据量,无需优化 |
5.2 实际测试案例
假设有一个包含 10,000 个元素的数组(含大量重复值),通过以下代码测试不同方法的执行时间:
const arr = Array.from({ length: 10000 }, () => Math.floor(Math.random() * 1000));
console.time('Set method');
[...new Set(arr)];
console.timeEnd('Set method'); // ~0.1ms
console.time('Hash method');
const hash = {};
arr.forEach(item => { hash[item] = true; });
Object.keys(hash).map(Number);
console.timeEnd('Hash method'); // ~0.1ms
console.time('Filter method');
arr.filter((item, index) => arr.indexOf(item) === index);
console.timeEnd('Filter method'); // ~50ms
结论:基于哈希表或 Set 的方法在大数据量下性能显著优于传统方法。
六、常见问题与解决方案
6.1 处理多维数组或对象
当需要去重的对象或嵌套数组时,需先序列化为字符串:
const arr = [
{ name: 'Alice', age: 20 },
{ name: 'Bob', age: 25 },
{ name: 'Alice', age: 20 }
];
const unique = arr => {
const seen = new Set();
return arr.filter(item => {
const key = JSON.stringify(item);
return seen.has(key) ? false : seen.add(key);
});
};
console.log(unique(arr)); // 只保留前两个对象
6.2 保留首次出现的元素
多数方法默认保留首次出现的元素,但若需保留最后一次出现的元素,可调整逻辑:
function keepLast(arr) {
const seen = {};
const result = [];
for (const item of arr) {
seen[item] = item; // 覆盖前值
}
return Object.values(seen);
}
结论
JavaScript 数组去重是一个看似简单却充满细节的课题。开发者需要根据具体场景选择合适的方法:
- 基础场景:使用
Set
或哈希表实现高效去重。 - 兼容性要求高:采用对象键存储法或优化后的
for
循环。 - 复杂对象处理:结合 JSON 序列化与 Map 结构。
通过理解不同方法的底层原理与性能差异,开发者可以编写出既简洁又高效的代码。记住,没有“万能”的去重方案,选择始终取决于数据特性、代码环境与性能需求。希望本文能为你提供清晰的思路与实用的解决方案!