javascript 数组去重怎么写?附 6 种方法+对象数组场景对比清单

编程狮(w3cschool.cn) 2026-09-01 10:23:36 浏览数 (122)
反馈

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

JavaScript 数组去重方法图

一、去重到底在去什么

数组去重 = 让「内容相同」的元素只保留一个。比喻:像收快递,同一个地址的包裹你只想算一次,重复的退掉。

⚠️ 注意:「相同」指严格相等(===)。{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 即真)"碰巧"能跑通,但可读性极差,是典型的反模式,别在团队代码里这么写。

💡 小提示:includesindexOfNaN 的处理不同——includes 能认出 NaNindexOf 不能。后果比想象中严重,看这段实测:

javascript
const a = [NaN, 1, NaN, 2];
a.filter((x, i) =& a.indexOf(x) === i); // [1, 2] —— NaN 被整条丢掉了


注意这不是「没去重成功」,而是数据静默丢失indexOf 永远返回 -1,于是每个 NaN 的下标都对不上,全部被过滤掉。所以数组里可能出现 NaN(比如浮点运算的失败结果)时,务必用 includesSet,别用 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,让后到的覆盖先到的——不是"把判断反过来"。

JavaScript 数组去重方法怎么选

总结

数组去重,日常首选 [...new Set(arr)],一行、快、好读;要兼容老环境用 filter+indexOf;对象数组按某个字段去重用 Map。想系统补齐 JS 基础,可以看 JS 参考手册 把数组与集合类串起来。

要点带走:

  • Set 是最简方案,但「相同」按 === 判断;
  • 对象数组必须按字段去重,Set 直接去不掉;
  • 大数组避开 O(n²) 写法,优先 Set / Map。

延伸学习

想把这块知识系统补齐,可以按这个顺序来:

  1. 先过一遍 JavaScript 数组与 ES6 配套课程,把数组与 ES6 铺平;
  2. 想看另一种讲法,参考 JavaScript 数组去重方法笔记 的 4 种方法;
  3. 写完想立刻试,在线运行 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 最稳。

0 人点赞