想去掉 JavaScript 数组里的重复元素,最省事的做法是用 Set:把数组转成 Set 再转回数组,一行就能得到不重复的版本。本文会先讲清楚去重到底在做什么、什么情况下算重复,再依次演示 Set、filter 加 indexOf、reduce、for 循环加 includes 四种方法,并说明对象数组如何按字段去重,帮你按真实场景选对最顺手的写法,而不是死记某一种。无论你是刚学数组的新手,还是想补全工具箱的老手,都能从里找到适合自己的那一种。
一、先搞清楚:去重到底在做什么
数组去重,字面意思就是「把一个数组里重复出现的值只保留一份」。比如原始数组是 [1, 2, 2, 3, 3, 3],去重之后应该得到 [1, 2, 3]。
为什么这个需求这么常见?因为数据往往不是你亲手一个个写死的,而是来自接口返回、用户勾选、或者多次合并。比如一个多选框用户不小心点了两次同一个选项,后端吐回来的列表里就出现了重复;再比如把两页数据拼到一起,难免有重叠的条目。在把这些数据拿去渲染、统计、或者提交之前,先做一次数组去重,能避免很多莫名其妙的 bug。
要提醒一点:JavaScript 里的「相等」是按严格相等来判断的。数字 1 和字符串 "1" 不算重复,两个内容相同但分别 new 出来的对象也不算重复。所以下面所有方法,默认都是对「基本类型」(数字、字符串、布尔值)去重;对象数组要单独处理,这一点放在最后一节讲。如果你刚接触数组,建议先把 JavaScript 教程 里数组这一章过一遍,理解引用和值的区别,后面才不会困惑。
还有一个常见误区要提前说:有人会先 sort 排序、再遍历去重。这确实能实现,但既改掉了原数组的顺序,又多了一层排序开销;除非你本来就要排序,否则不如直接用 Set 来得干净。去重的目标是「保序且去重」,而不是「顺便排个序」。
二、方法一:用 Set 一行搞定(最推荐)
Set 是 JavaScript 内置的一种数据结构,它的最大特点就是「成员不能重复」。任何放进去的值,只要已经存在,就会被自动丢弃。利用这个特性,数组去重可以压缩成一行:
const arr = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3]
原理分三步:先用 new Set(arr) 把数组变成 Set,重复项在转换时就被过滤掉了;再用扩展运算符 ... 把 Set 拆开;最后用方括号包起来变回数组。如果你不喜欢扩展运算符,也可以写成 Array.from(new Set(arr)),效果完全一样。
这是日常开发里用得最多、也最不容易出错的方式。它的优点是一行解决、语义清晰、性能好;缺点是「黑盒」了一点,新手可能一时想不明白中间发生了什么。当你只是想快速得到一个干净的数组时,闭眼用这一招就行。语法细节记不住时,随时翻 JavaScript 速查手册 查,里面有 Set 与常用数组方法的一览。
三、方法二:filter + indexOf(经典可控)
如果你想知道「到底哪些被去掉了」,或者团队代码规范不鼓励用 Set,那么 filter 配合 indexOf 是教科书级的写法:
const arr = [1, 2, 2, 3, 3, 3];
const unique = arr.filter((item, index) => arr.indexOf(item) === index);
console.log(unique); // [1, 2, 3]
这段代码的逻辑是:遍历数组,对每一个元素,都去问一句「你第一次出现的位置,是不是就是现在这个位置?」indexOf 永远返回某个值「第一次出现」的下标,只有当当前下标正好等于第一次出现的位置时,才说明这是该值的「第一个副本」,把它保留;后续重复出现的副本因为下标对不上,就被 filter 过滤掉了。
这种写法胜在过程透明,每一步你都能在脑子里推演。缺点是效率略低:数组越长,indexOf 被调用的次数越多,整体是平方级的开销。日常几千条以内的数据完全够用,但如果是几十万条的大数组,还是用 Set 更稳。另外要注意,indexOf 做的是严格相等比较,所以它对基本类型可靠,对对象依然无效。
四、方法三、四:reduce 与 for 循环,以及对象数组怎么按字段去重
除了上面两种,还有两种值得知道。第一种是用 reduce 边遍历边往结果里塞:
const arr = [1, 2, 2, 3, 3, 3];
const unique = arr.reduce((acc, item) => {
if (!acc.includes(item)) acc.push(item);
return acc;
}, []);
console.log(unique); // [1, 2, 3]
reduce 接收一个累加器 acc 和当前元素 item,每次判断 acc 里有没有 item,没有才 push 进去。逻辑和 for 循环几乎一样,只是写成了函数式风格。
第二种是最朴素的 for 循环加 includes:
const arr = [1, 2, 2, 3, 3, 3];
const unique = [];
for (const item of arr) {
if (!unique.includes(item)) unique.push(item);
}
console.log(unique); // [1, 2, 3]
它和 reduce 思路相同,只是用循环写出来,对刚入门、还不熟悉高阶函数的同学更友好。
最后说对象数组。下面这几种方法对「两个看起来一样的对象」是去不掉重的,因为对象是引用类型。要按某个字段去重,比如按 id 去重,得显式判断:
const list = [
{ id: 1, name: "A" },
{ id: 1, name: "A" },
{ id: 2, name: "B" },
];
const seen = new Set();
const unique = list.filter((item) => {
if (seen.has(item.id)) return false;
seen.add(item.id);
return true;
});
console.log(unique); // [{id:1,name:"A"}, {id:2,name:"B"}]
这里用 Set 记录已经见过的 id,只保留每个 id 第一次出现的对象。把 id 换成 name 或任何你想去重的字段都可以。遇到拿不准的术语,编程词典 里有通俗解释,比如 Set、引用类型这些概念都有专门说明。
从性能角度给个大致排序:Set 最快(底层哈希),for 循环加 includes 次之,filter 加 indexOf 与 reduce 因为每次都要扫描已结果集,在超大数组上最慢。日常数据量下差异可以忽略,但如果你在处理十万级以上的数据,优先选 Set。
总结
数组去重是 JavaScript 里最高频的小操作之一。记住一个总原则:日常基本类型去重,直接用 Set 一行搞定;想要过程透明或代码风格统一,用 filter + indexOf;对象数组则要按具体字段(如 id)手动判断。四种方法没有绝对的优劣,只有「当前场景哪个更顺手」。
延伸学习
常见问题
Q1:Set 去重后元素的顺序会变吗?
不会。Set 严格保留元素「第一次出现」的顺序,去重后顺序和原数组一致。
Q2:为什么两个内容相同的对象去重不掉?
因为对象是引用类型,JavaScript 判断相等看的是「是不是同一个引用」,而不是「内容一不一样」。要对对象去重必须按字段判断,见第四节。
Q3:NaN 能被 Set 正确去重吗?
可以。Set 内部对 NaN 做了特殊处理,哪怕 NaN === NaN 是 false,Set 也会把它当作同一个值,所以 [NaN, NaN] 用 Set 去重会得到 [NaN]。

免费 AI IDE



