接口返回了一串带重复 id 的列表,渲染前你想把重复项去掉,结果用 for 循环一顿操作,代码又长又易错。核心答案一句话:最省事的是 [...new Set(arr)],ES6 的 Set 天生不容许重复值,一行就能去重。本文基于现代浏览器与 Node.js 验证,覆盖 Set、filter+indexOf、filter+includes、reduce、for+includes、Map 六种写法,并单独讲「对象数组按某个字段去重」这个高频需求。看完你不仅能去重,还知道哪种写法该用在哪。今天这篇文章,编程狮就把这块讲透。

一、去重到底在去什么
数组去重 = 让「内容相同」的元素只保留一个。比喻:像收快递,同一个地址的包裹你只想算一次,重复的退掉。
⚠️ 注意:「相同」指严格相等(
===)。{a:1}和{a:1}是两个不同对象,Set 不会认为它们相同,这正是对象数组去重难的源头,后面专门讲。
1.1 触发条件
什么情况一定会用到去重:接口或爬虫拿到的列表有重复;多选标签用户不小心点了两次;合并两个数组后出现重叠;前端渲染前清理脏数据。这些都是日常开发里的高频场景。
1.2 常见误解
有人认为 indexOf 能找出所有重复。它只能返回「某元素第一次出现的位置」,处理重复要靠和「当前下标」比对,见方法二。也有人说「Set 一定无序」,那是旧规范的残留印象,现代引擎里 Set 是保序的,别被旧帖误导。
判断要不要去重,先看数据形态。如果是简单值数组([1,2,2,3]),Set 一行解决;如果是对象数组,必须按字段。还有一个隐藏场景是「嵌套去重」:比如接口返回的分类标签可能重复,渲染前先 Set 一下再展示,能避免界面出现两个一样的标签。去重和「排序」「分组」经常一起出现,理清这三者的关系,前端处理列表会顺手很多。另外,去重不一定发生在代码里——如果数据源头(数据库、接口)就能 DISTINCT 或聚合,在源头做往往比前端兜底更高效,前端去重更多是兜底和体验优化,别把它当成唯一手段。
二、方法一:Set(最推荐)
适合谁:大多数场景,要最省代码。代价:无法自定义「什么叫相同」。
const arr = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3]
上面这段用 Set 收下所有值(重复自动被吞掉),再用展开运算符 ... 变回数组。没系统学过 ES6 容器,可以先过一遍 JavaScript 教程 的数组章节。
选 Set 还是其他方法,先看你的约束。数据量小、追求代码最短,Set 一行足矣;要兼容老旧运行环境(比如某些老 IE),Set 不可用,就退回 filter+indexOf。如果你要的不是「去重」而是「按某种规则挑一个」,比如对象数组按 id 留最新一条,那 Set 不够用,得上 Map 或 reduce。一个常见误用是把 Set 当「深度去重」工具——它只认 === 引用,嵌套对象、不同引用的同值对象都去不掉,这点下面专门讲。性能上,Set 是 O(n) 且底层高度优化,几万条数据也秒回;而 indexOf/includes 系列是 O(n²),数据一大就明显变慢,别在循环里无脑用。实际项目里,去重往往和「排序」「分组」一起出现,理清三者关系,列表处理会顺手很多。
三、方法二:filter + indexOf
适合谁:环境不支持 Set(极老 IE),或想显式理解原理。代价:indexOf 是 O(n),整体 O(n²),大数据慢。
const arr = [1, 2, 2, 3, 3, 3];
const unique = arr.filter((item, idx) => arr.indexOf(item) === idx);
console.log(unique); // [1, 2, 3]
indexOf 返回「某元素第一次出现的位置」,只有当它等于「当前下标」时才保留,等于把后来的重复项过滤掉。语法细节可查 JavaScript 速查手册。
四、方法三到五:includes / reduce / for
用表格对比更清楚:
| 方法 | 写法核心 | 时间复杂度 | 5 万条实测 |
|---|---|---|---|
| (对照)Set | [...new Set(arr)] |
O(n) | 0.8 ms |
| filter + includes | !seen.includes(item) |
O(n²) | 124.8 ms |
| for + includes | 手动维护新数组 | O(n²) | 125.4 ms |
| reduce + 展开运算符 | acc.includes(item) ? acc : [...acc, item] |
O(n²) | 147.5 ms |
| reduce + Map | 累加器里存 Map | O(n) | 3.8 ms |
实测条件:5 万条数据、含 7000 个不同值、Node.js 22。不同机器跑出来的绝对值会有出入,但快慢的量级关系是一致的。
这三种写法其实都是 O(n²):每来一个元素,都要把已收集的结果从头扫一遍(includes / indexOf 都是线性扫描)。其中 reduce 那种最慢——它每次还要用展开运算符 [...acc, item] 把整个数组复制一遍,等于在平方之上又叠了一层。想保留「一行流」的写法又要 O(n),得让累加器存 Map 而不是数组。
const arr = [1, 2, 2, 3, 3, 3];
const seen = [];
const u3 = arr.filter(item => {
if (seen.includes(item)) return false;
seen.push(item);
return true;
});
// 方法四:reduce + 展开运算符(写起来短,但最慢)
const u4 = arr.reduce((acc, item) => acc.includes(item) ? acc : [...acc, item], []);
// 方法五:for + includes
const u5 = [];
for (const item of arr) if (!u5.includes(item)) u5.push(item);
// 想要 O(n) 的一行流:让累加器存 Map,而不是数组
const u6 = [...new Map(arr.map(item => [item, item])).values()];
顺带避个坑:上面的 filter 写法别为了短写成 arr.filter(item => seen.includes(item) ? false : seen.push(item))。它靠 push 返回数组长度(非 0 即真)"碰巧"能跑通,但可读性极差,是典型的反模式,别在团队代码里这么写。
💡 小提示:
includes和indexOf对NaN的处理不同——includes能认出NaN,indexOf不能。后果比想象中严重,看这段实测:
javascript
const a = [NaN, 1, NaN, 2];
a.filter((x, i) =& a.indexOf(x) === i); // [1, 2] —— NaN 被整条丢掉了
注意这不是「没去重成功」,而是数据静默丢失:indexOf永远返回-1,于是每个NaN的下标都对不上,全部被过滤掉。所以数组里可能出现NaN(比如浮点运算的失败结果)时,务必用includes或Set,别用indexOf那套。
五、对象数组按字段去重(高频)
真实业务里往往是 [{id:1,...},{id:1,...}],按 id 去重:
const users = [{id:1,name:'a'},{id:1,name:'b'},{id:2,name:'c'}];
const map = new Map();
for (const u of users) if (!map.has(u.id)) map.set(u.id, u);
const uniqueUsers = [...map.values()];
console.log(uniqueUsers); // 只剩 id 为 1 和 2 的各一条
这里有几个细节值得记牢。第一,Map 去重默认是「先到先得」:谁先 set 进 Map 谁留下,后到的同名 id 被忽略。如果你想「保留最后一条」,做法不是"把判断反过来",而是直接删掉 if (!map.has(u.id)) 这层判断,写成 for (const u of users) map.set(u.id, u);——后 set 的会覆盖先 set 的,自然就留下了最后一条。(原写法本身就已经是"先 has 就跳过",照着"反过来"做只会写出同一段代码,拿不到最后一条。)第二,Map 会保持插入顺序,去重后 [...map.values()] 的顺序和你遍历原数组的顺序一致,可控可预期。第三,如果数据来自 JSON 接口,先 JSON.parse 再处理,别对字符串去重,那是两种完全不同的事。第四,超大对象数组(十万级以上)去重时,Map 仍是最优解,它底层是哈希表,O(n) 即可完成,远比嵌套循环稳。把字段选对、顺序想清,对象数组去重其实就这一招。
踩坑清单:
- 顺序问题:Set 在现代引擎保序,但老资料说「Set 无序」,别被旧帖误导。
- 引用类型:
[{},{}]用 Set 去不掉,必须按字段(如上)。 - 性能:大数组别用 O(n²) 的 indexOf/includes 写法,万级以上数据请用 Set 或 Map。
- 保留哪一条:按字段去重默认「先到先得」。想保留最后一条,去掉
if (!map.has(...))判断直接map.set,让后到的覆盖先到的——不是"把判断反过来"。

总结
数组去重,日常首选 [...new Set(arr)],一行、快、好读;要兼容老环境用 filter+indexOf;对象数组按某个字段去重用 Map。想系统补齐 JS 基础,可以看 JS 参考手册 把数组与集合类串起来。
要点带走:
Set是最简方案,但「相同」按===判断;- 对象数组必须按字段去重,Set 直接去不掉;
- 大数组避开 O(n²) 写法,优先 Set / Map。
延伸学习
想把这块知识系统补齐,可以按这个顺序来:
- 先过一遍 JavaScript 数组与 ES6 配套课程,把数组与 ES6 铺平;
- 想看另一种讲法,参考 JavaScript 数组去重方法笔记 的 4 种方法;
- 写完想立刻试,在线运行 JavaScript 代码 免安装跑片段。
常见问题
Q:Set 去重后顺序会变吗?
A:现代浏览器和 Node.js 里 Set 会保持插入顺序,去重后顺序和原数组一致。早年资料说「无序」是针对旧规范,现已不适用,可以放心依赖顺序。
Q:对象数组用 Set 为什么去不掉?
A:因为两个 {id:1} 是两个不同对象,=== 比较的是引用而非内容,Set 认为它们不一样。必须像正文那样按 id 等字段用 Map 去重,或者用 reduce 配合「已见集合」手动过滤。
Q:NaN 在数组里怎么去重?
A:用 includes 系列写法,[...new Set([NaN, NaN])] 结果也是 [NaN],Set 本身能正确处理 NaN。真正要躲的是 indexOf:它认不出 NaN,会让所有 NaN 元素被静默丢弃——[NaN, 1, NaN, 2] 走 filter + indexOf 之后只剩 [1, 2]。所以数组可能含 NaN 时(比如浮点运算结果),统一走 Set 或 includes 最稳。

免费 AI IDE



