前端数组去重:3 种常用方法原理与实战解析
在前端开发中,数组去重是高频出现的需求 —— 无论是处理接口返回的重复数据、过滤用户输入的重复选项,还是整理本地存储的列表数据,都需要通过去重保证数据的唯一性。本文将聚焦三种最常用的数组去重方法:Set 方法、filter + indexOf 方法、reduce 方法,从原理拆解到代码实战,带你彻底掌握数组去重的逻辑与应用场景。
一、Set 方法:最简单高效的去重
Set 是 ES6 新增的数据结构,其核心特性是 “只存储不重复的值”,这让它成为数组去重的 “最优解” 之一 —— 代码简洁、执行效率高,几乎是日常开发的首选方案。
1. 原理拆解
- Step 1:将数组转为 Set:由于 Set 不允许存储重复元素,当我们把数组作为参数传入 new Set() 时,重复的元素会被自动过滤,最终得到一个只包含唯一值的 Set 实例。
- Step 2:将 Set 转回数组:Set 不是数组,无法直接使用数组的方法(如 map、forEach),因此需要通过 扩展运算符(...) 或 Array.from() 方法,将 Set 实例重新转为数组。
2. 代码实战
const originalArr = [1, 2, 2, '3', '3', true, true, null, null, undefined, undefined];// 方法1:扩展运算符(...)转数组
const uniqueArr1 = [...new Set(originalArr)];
console.log(uniqueArr1);// 方法2:Array.from() 转数组
const uniqueArr2 = Array.from(new Set(originalArr));
console.log(uniqueArr2);
效果:
3.优缺点分析
- 优点:
- 代码极简:一行代码即可完成去重,可读性强;
- 效率高:Set 基于哈希表实现,去重过程的时间复杂度接近 O (n);
- 支持多类型:能处理数字、字符串、布尔值、null、undefined 等常见类型的重复元素。
- 缺点:
- 无法区分 “值相等但类型不同” 的元素:例如 1 和 '1' 会被视为不同元素(符合 JS 类型严格比较规则,但需注意业务场景);
- 无法处理复杂数据类型:如对象({id: 1})、数组([1,2]),因为 Set 判断重复的依据是 “严格相等(===)”,而不同引用的复杂类型即使内容相同,也会被视为不同元素。
二、filter + indexOf 方法:基于 “索引判断” 的经典方案
filter 是数组的遍历过滤方法,indexOf 用于查找元素在数组中 “第一次出现的索引”—— 两者结合的核心逻辑是:只保留 “当前元素第一次出现” 的项,从而实现去重。
1. 原理拆解
- Step 1:遍历数组(filter):filter 会遍历数组中的每个元素,对每个元素执行一个 “判断函数”,最终返回所有 “判断为 true” 的元素组成的新数组。
- Step 2:判断元素是否首次出现(indexOf):indexOf(item) 会返回 item 在数组中第一次出现的索引。如果 “当前元素的索引(index)” 等于 “第一次出现的索引”,说明该元素是首次出现,保留;反之则是重复元素,过滤。
2.代码实战
const originalArr = [1, 2, 2, 3, 3, 3, 'a', 'a'];
const uniqueArr = originalArr.filter((item, index, arr) => {return arr.indexOf(item) === index;
});
console.log(uniqueArr);
效果:
3.优缺点分析
- 优点
- 无需依赖 ES6 特性:兼容性好,支持 IE8 及以上。
- 缺点:
- 效率较低:indexOf 本质是 “遍历数组查找索引”,嵌套在 filter 的遍历中,整体时间复杂度为 O (n²),数据量大时(如万级以上数组)会明显卡顿;
- 同样无法处理复杂类型:indexOf 判断重复的依据也是 “严格相等(===)”,无法识别内容相同的对象 / 数组。
三、reduce 方法:基于 “累加器” 的灵活方案
reduce 是数组的 “归约” 方法,核心作用是 “将数组元素逐步累加为一个单一值”—— 在去重场景中,这个 “单一值” 就是我们需要的 “唯一数组”,通过判断元素是否已在累加器中,实现去重。
1. 原理拆解
- Step 1:初始化累加器(acc):reduce 的第一个参数是 “累加器函数”,第二个参数是 “累加器的初始值”(这里我们初始化为空数组 [])。
- Step 2:遍历并判断元素(includes):遍历数组时,对每个元素 item,用 acc.includes(item) 判断 “累加器中是否已包含该元素”:
- 若不包含:将 item 加入累加器(返回 [...acc, item]);
- 若已包含:直接返回当前累加器(不做修改)。
- Step 3:返回最终累加器:遍历结束后,累加器就是去重后的唯一数组。
2.代码实战
const originalArr = [10, 20, 20, 40, 40, 40, 'b', 'b', 'b'];const uniqueArr = originalArr.reduce((acc, item) => {if (!acc.includes(item)) {acc.push(item);}return acc;
}, []);console.log(uniqueArr);
效果:
3. 优缺点分析
- 优点:
- 灵活性高:reduce 不仅能去重,还能在去重过程中同步处理其他逻辑(如统计元素出现次数、过滤特定值);
- 纯函数特性:若使用 return [...acc, item] 而非 acc.push(item),可避免修改原累加器,符合函数式编程的 “无副作用” 原则。
- 缺点:
- 效率问题:includes 本质也是 “遍历累加器查找元素”,嵌套在 reduce 的遍历中,时间复杂度同样为 O (n²),大数据场景下性能一般;
- 复杂类型支持有限:与前两种方法一致,无法处理内容相同但引用不同的对象 / 数组。
四、拓展处理 “复杂数据类型” 去重
前面三种方法都无法处理 “对象 / 数组” 等复杂类型的去重(例如 [{id:1}, {id:1}]),此时需要自定义 “重复判断规则”—— 核心思路是 “将复杂类型转为可比较的唯一标识”,再进行去重。
以 “对象数组按 id 去重” 为例,实现方案如下:
const objArr = [{id:1, name:'A'}, {id:2, name:'B'}, {id:1, name:'A'}];const uniqueObjArr = [...new Set(objArr.map(item => item.id))].map(id => objArr.find(item => item.id === id));console.log(uniqueObjArr);
效果:
原理:先通过 map 将对象数组转为 “id 数组”,用 Set 去重后,再通过 find 找到每个 id 对应的原始对象,最终得到去重后的对象数组。