JS去除数组重复项的5种方法
从现在开始,我们要努力学习啦!今天我给大家带来《JS删除数组重复项的几种方法》,感兴趣的朋友请继续看下去吧!下文中的内容我们主要会涉及到等等知识点,如果在阅读本文过程中有遇到不清楚的地方,欢迎留言呀!我们一起讨论,一起学习!
最直接、最现代的JavaScript数组去重方法是使用Set,因其设计初衷即为存储唯一值,可高效去除基本类型重复项;2. 对于对象数组去重,需基于唯一标识属性结合Map实现,或通过自定义比较逻辑处理复杂场景;3. 需警惕类型隐式转换、NaN特殊性等潜在陷阱,并根据数据规模权衡性能与可读性,确保明确“重复”定义后再选择合适方案。
要说JavaScript里怎么去掉数组重复项,最直接、最现代的答案就是用Set
。它就像个天然的去重机,一丢进去,出来就都是独一无二的了。

解决方案
在我看来,处理数组去重,最优雅且效率不错的方案,首推ES6的Set
对象。它的设计初衷就是为了存储不重复的值,所以用它来去重简直是量身定制。
使用 Set
对象

这种方法简洁明了,可读性极高。
function uniqueArrayWithSet(arr) { return [...new Set(arr)]; // 或者:return Array.from(new Set(arr)); } const numbers = [1, 2, 2, 3, 4, 4, 5]; const uniqueNumbers = uniqueArrayWithSet(numbers); console.log(uniqueNumbers); // 输出: [1, 2, 3, 4, 5] const strings = ['apple', 'banana', 'apple', 'orange']; const uniqueStrings = uniqueArrayWithSet(strings); console.log(uniqueStrings); // 输出: ['apple', 'banana', 'orange']
这种方式的优点是代码量少,语义清晰,而且内部实现通常是经过优化的,对于大多数情况性能表现都很好。它能处理基本数据类型(数字、字符串、布尔值、null
、undefined
)的去重。不过,对于对象(Object),Set
是基于引用的去重,这意味着 {a:1}
和 {a:1}
会被视为两个不同的值,因为它俩在内存中的引用地址不同。这一点在使用时需要特别注意。

当然,除了 Set
这种“现代”方式,JavaScript 发展这么多年,也积累了不少“传统”的去重手段,它们在特定场景下依然有用,或者说,理解它们能帮助我们更好地理解数组操作的底层逻辑。
为什么数组去重是个常见需求?(以及我们为什么要关心效率)
说实话,我在日常开发中,遇到数组去重的场景简直太多了。这并不是一个孤立的问题,它几乎渗透在数据处理的方方面面。比如,你可能从后端API拿了一堆数据,结果发现某些ID或者标签重复了,你肯定不希望在前端展示的时候也重复吧?又或者,用户在一个多选框里不小心点了两次同一个选项,你后台接收到的数组里就有了重复项。再比如,当你合并多个数组时,为了保持数据的纯粹性,去重就成了必不可少的一步。
为什么要关心效率?这其实是个很实际的问题。如果你的数组只有几十个、几百个元素,那么用什么方法去重,性能差异几乎可以忽略不计。但如果你的数组有几万、几十万甚至上百万个元素,那么一个O(N²)复杂度的算法,和O(N)或者O(N log N)的算法,其执行时间可能就是几毫秒和几秒甚至几十秒的区别。在用户体验至上的今天,没有人愿意等待一个慢吞吞的页面。所以,选择一个合适的、高效的去重方法,不仅仅是代码写得好不好看的问题,更是直接影响产品性能和用户体验的关键。这就像你在修一条路,小路可能随便铺铺就行,但要是修高速公路,你肯定要考虑材料、施工方式和未来的承载能力,对吧?
除了Set,还有哪些经典的JavaScript数组去重方法?(性能与场景考量)
除了 Set
这个“万金油”,我们还有一些其他经典的去重方法,它们各有特点,也适合不同的场景。理解它们能帮助你更灵活地应对各种去重需求。
1. 使用 filter()
结合 indexOf()
这是非常经典的一种方式,也相对容易理解。filter()
方法会创建一个新数组,其中包含通过所提供函数实现的测试的所有元素。而 indexOf()
则会返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1。
function uniqueArrayWithFilterAndIndexOf(arr) { return arr.filter((item, index, self) => { return self.indexOf(item) === index; }); } const numbers = [1, 2, 2, 3, 4, 4, 5]; const uniqueNumbers = uniqueArrayWithFilterAndIndexOf(numbers); console.log(uniqueNumbers); // 输出: [1, 2, 3, 4, 5]
性能考量: 这种方法对于每个元素,都需要遍历(或至少部分遍历)数组来查找其首次出现的位置。因此,它的时间复杂度是 O(N²)。对于小型数组(几百个元素以内),这种性能开销通常可以接受,代码也比较直观。但如果数组非常大,性能瓶颈就会非常明显,我个人是不太推荐在大规模数据处理中使用它。
2. 使用 reduce()
结合 includes()
或 Map
/Object
reduce()
方法可以把数组“规约”成一个单一的值,这里我们可以用它来构建一个不重复的新数组。
// 方法A: 结合 includes() function uniqueArrayWithReduceAndIncludes(arr) { return arr.reduce((accumulator, current) => { if (!accumulator.includes(current)) { accumulator.push(current); } return accumulator; }, []); } const numbers = [1, 2, 2, 3, 4, 4, 5]; const uniqueNumbers = uniqueArrayWithReduceAndIncludes(numbers); console.log(uniqueNumbers); // 输出: [1, 2, 3, 4, 5]
性能考量: 类似 filter
+ indexOf
,includes()
也会在每次迭代时遍历 accumulator
数组,所以这种方法的平均时间复杂度也是 O(N²)。
方法B: 结合 Map
或普通 Object
(作为哈希表)
这种方式的思路是利用 Map
或普通对象的键值对特性,将数组中的元素作为键存储,从而达到去重的目的。因为 Map
或对象查找键的效率非常高(接近 O(1)),所以整体性能会好很多。
function uniqueArrayWithReduceAndMap(arr) { const map = new Map(); return arr.reduce((accumulator, current) => { if (!map.has(current)) { map.set(current, true); // 存入Map,标记已见过 accumulator.push(current); } return accumulator; }, []); } const numbers = [1, 2, 2, 3, 4, 4, 5]; const uniqueNumbers = uniqueArrayWithReduceAndMap(numbers); console.log(uniqueNumbers); // 输出: [1, 2, 3, 4, 5] // 也可以直接用Object,但Map在键类型和性能上更灵活 function uniqueArrayWithObject(arr) { const obj = {}; const result = []; for (let i = 0; i < arr.length; i++) { const item = arr[i]; if (!obj[item]) { // 判断是否已存在 obj[item] = true; result.push(item); } } return result; } const moreNumbers = [1, 2, 2, 3, 4, 4, 5, '1', 'a', 'a']; // 注意 '1' 和 1 的区别 const uniqueMoreNumbers = uniqueArrayWithObject(moreNumbers); console.log(uniqueMoreNumbers); // 输出: [1, 2, 3, 4, 5, "1", "a"]
性能考量: 这种方法的时间复杂度接近 O(N),因为 Map.has()
或对象属性查找的平均时间是常数时间。这使得它在处理大型数组时,性能表现非常接近 Set
,是除了 Set
之外我个人比较推荐的通用去重方案。
总结一下:
Set
: 最佳选择,代码简洁,性能优异(O(N)),但要注意对象去重是基于引用的。filter
+indexOf
/reduce
+includes
: 代码直观,但性能较差(O(N²)),不适合大型数组。reduce
+Map
/Object
: 性能优异(O(N)),可以作为Set
的替代,尤其是在需要兼容旧环境,或者对键有特殊处理需求时。
选择哪种方法,说到底还是要看你的具体需求:数组大小、数据类型、以及对代码简洁性或兼容性的偏好。
处理包含对象的数组去重:一个更复杂的挑战
前面我们讨论的去重方法,对于基本数据类型(数字、字符串等)都很有效。但当数组里装的是对象时,事情就变得有点复杂了。这是因为 JavaScript 在比较对象时,默认是比较它们的引用地址,而不是它们内部的属性值。也就是说,{ id: 1, name: 'A' }
和 { id: 1, name: 'A' }
在内存中是两个不同的对象,即使它们的属性值完全一样,Set
也会认为它们是不同的。这就像你有两张一模一样的照片,但它们是不同的冲印出来的纸张,不是同一张。
那么,如果我们想根据对象的某个或某几个属性来判断“重复”,该怎么办呢?
1. 根据唯一标识属性去重 (推荐)
如果你的对象有一个或多个可以作为唯一标识的属性(比如 id
、uuid
、sku
等),这是最常用也最可靠的方法。我们可以利用 Map
来存储这些唯一标识,并把对应的对象存起来。
function uniqueObjectsById(arr, key) { const map = new Map(); const result = []; for (const item of arr) { // 确保对象有这个key,并且这个key的值不为空 if (item && item[key] !== undefined && !map.has(item[key])) { map.set(item[key], item); // 以key的值作为Map的键 result.push(item); } } return result; } const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 1, name: 'Alicia' }, // id重复,但name不同 { id: 3, name: 'Charlie' }, { id: 2, name: 'Bobby' } // id重复,但name不同 ]; const uniqueUsers = uniqueObjectsById(users, 'id'); console.log(uniqueUsers); /* 输出: [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' } ] */
这种方法非常高效,因为它利用了 Map
的 O(1) 查找特性。它会保留第一个遇到的具有该 id
的对象。
2. 将对象转换为字符串进行比较 (有风险,慎用)
如果你没有一个明确的唯一标识符,或者需要根据对象的所有属性来判断重复,一个“歪招”是把对象转换成字符串(比如用 JSON.stringify
),然后对这些字符串进行去重。
function uniqueObjectsByStringify(arr) { const stringifiedSet = new Set(); const result = []; for (const item of arr) { const stringifiedItem = JSON.stringify(item); if (!stringifiedSet.has(stringifiedItem)) { stringifiedSet.add(stringifiedItem); result.push(item); } } return result; } const data = [ { a: 1, b: 2 }, { b: 2, a: 1 }, // 属性顺序不同,JSON.stringify结果可能不同! { a: 1, b: 2 }, { c: 3, d: 4 } ]; const uniqueData = uniqueObjectsByStringify(data); console.log(uniqueData); /* 输出: [ { a: 1, b: 2 }, { b: 2, a: 1 }, // 注意这里,如果属性顺序不同,会被认为是两个不同的对象 { c: 3, d: 4 } ] */
风险点:
- 属性顺序:
JSON.stringify
的结果会受对象属性顺序的影响。{a:1, b:2}
和{b:2, a:1}
转换成的字符串是不同的,即使它们在逻辑上是“相同”的对象。 - 循环引用: 如果对象中存在循环引用,
JSON.stringify
会抛出错误。 - 不可序列化的值: 如果对象包含
undefined
、函数、Symbol
值,或者BigInt
,JSON.stringify
会忽略它们或抛出错误。
所以,这种方法只适用于非常简单的、属性顺序固定的、不含特殊值的对象数组。在我看来,除非你对数据结构有绝对的掌控,否则尽量避免这种方式。
3. 自定义比较函数 (更灵活但复杂)
对于更复杂的场景,比如需要根据多个属性组合判断唯一性,或者属性值本身也是对象,你可能需要编写一个自定义的比较函数,然后结合 filter
或 reduce
来实现。
function uniqueObjectsByCustomLogic(arr, compareFn) { const result = []; arr.forEach(item => { // 检查结果数组中是否已存在与当前item“相同”的元素 const isDuplicate = result.some(existingItem => compareFn(existingItem, item)); if (!isDuplicate) { result.push(item); } }); return result; } // 示例比较函数:根据 id 和 type 两个属性判断 const compareUsers = (user1, user2) => user1.id === user2.id && user1.type === user2.type; const complexUsers = [ { id: 1, type: 'admin', name: 'A' }, { id: 2, type: 'user', name: 'B' }, { id: 1, type: 'admin', name: 'C' }, // id和type都重复 { id: 1, type: 'guest', name: 'D' } // id重复,但type不同 ]; const uniqueComplexUsers = uniqueObjectsByCustomLogic(complexUsers, compareUsers); console.log(uniqueComplexUsers); /* 输出: [ { id: 1, type: 'admin', name: 'A' }, { id: 2, type: 'user', name: 'B' }, { id: 1, type: 'guest', name: 'D' } ] */
性能考量: 这种方法因为 some()
内部的循环,以及 compareFn
的执行,性能通常是 O(N²)。对于大型数组,需要慎重考虑。但它的优势在于极高的灵活性,你可以根据任何复杂的逻辑来定义“重复”。
总的来说,处理对象数组去重,核心思想就是找到一个可靠的“判重”依据。如果能通过某个唯一ID,那用 Map
绝对是首选。如果不行,就得根据具体业务逻辑来权衡是接受 JSON.stringify
的局限性,还是投入精力去写一个更复杂的自定义比较函数。
数组去重时可能遇到的“坑”和最佳实践
在处理数组去重时,虽然方法很多,但有些小细节或者“坑”是需要注意的。这些东西往往不是代码本身的问题,而是数据特性或者我们对数据理解的偏差导致的。
1. 数据类型隐式转换的陷阱
JavaScript 的类型转换有时会让人头疼。比如,当你用 indexOf
或者 Set
去重时,1
和 '1'
是被认为是不同的值。但如果你用 Object
作为哈希表,并且键是数字,那么 obj[1]
和 obj['1']
可能会指向同一个属性,因为对象键会被强制转换为字符串。
const mixedArray = [1, '1', 2, '2', 1]; const uniqueWithSet = [...new Set(mixedArray)]; console.log(uniqueWithSet); // [1, "1", 2, "2"] - Set区分了数字和字符串 const uniqueWithObjectHash = (() => { const obj = {}; const result = []; for (const item of mixedArray) { // obj[item] 会将 item 转换为字符串作为键 // obj[1] 和 obj['1'] 都会变成 obj['1'] if (!obj[item]) { obj[item] = true; result.push(item); } } return result; })(); console.log(uniqueWithObjectHash); // [1, 2] - '1' 和 '2' 被视为重复
这里 uniqueWithObjectHash
的结果可能会出乎意料,因为它将 1
和 '1'
视为相同。所以,在去重前,最好确保你的数组元素类型是一致的,或者你清楚这种类型转换带来的影响。
2. NaN
的特殊性
NaN
(Not a Number)是一个非常特殊的值。在 JavaScript 中,NaN
和任何值都不相等,包括它自己 (NaN === NaN
返回 false
)。这意味着,如果你数组里有多个 NaN
,indexOf
会认为它们都是不同的。
const nanArray = [1, NaN, 2, NaN, 3]; const uniqueWithFilter = nanArray.filter((item, index, self) => self.indexOf(item) === index); console.log(uniqueWithFilter); // [1, NaN, 2, NaN, 3] - 两个NaN都保留了 const uniqueWithSet = [...new Set(nanArray)]; console.log(uniqueWithSet); // [1, NaN, 2, 3] - Set 对 NaN 的处理是特殊的,它只会保留一个 NaN
Set
在处理 NaN
时表现得更“智能”,它只会存储一个 NaN
。这是 Set
的一个优点,但如果你不了解,可能会感到困惑。
3. 性能与可读性的权衡
前面提到了各种方法的性能差异。在实际开发中,我们总是在性能和代码可读性之间做权衡。对于小数组,我个人倾向于选择最直观、最简洁的 Set
方法,或者 filter
+ indexOf
,因为它们的性能开销几乎可以忽略,而代码维护成本低。只有当面对性能瓶颈时,才会去考虑更复杂的优化,比如手动构建哈希表。过度优化一个非瓶颈点,反而会增加代码的复杂性。
4. 保持数组的原始顺序
大部分去重方法都会保留元素在原数组中的首次出现顺序,比如 Set
、filter
、reduce
。但如果你自己实现一些基于排序的去重(比如先排序,再遍历去重),那么原始顺序就会丢失。这在某些业务场景下可能是不可接受的。
5. 明确“重复”的定义
尤其是在处理对象数组时,最关键的一点就是:你到底如何定义“重复”?是所有属性都相同才算重复?还是某个ID相同就算重复?这个定义直接决定了你选择哪种去重策略,以及如何编写你的比较逻辑。没有明确的定义,再好的去重方法也可能无法满足你的需求。
在我看来,最好的实践是:
- 优先使用
Set
,
今天关于《JS去除数组重复项的5种方法》的内容介绍就到此结束,如果有什么疑问或者建议,可以在golang学习网公众号下多多回复交流;文中若有不正之处,也希望回复留言以告知!

- 上一篇
- jQuery跳跃动画实现与animate()常见问题解析

- 下一篇
- Python代码审计:AST遍历技巧解析
-
- 文章 · 前端 | 55秒前 |
- HTML5WebSocket使用教程详解
- 237浏览 收藏
-
- 文章 · 前端 | 1分钟前 | SEO 标签 HTML页面标题 document.title
- 设置HTML标题的两种方式
- 444浏览 收藏
-
- 文章 · 前端 | 3分钟前 |
- HTML5inputrequired属性使用教程
- 463浏览 收藏
-
- 文章 · 前端 | 4分钟前 |
- 事件循环与异步编程如何协同工作
- 330浏览 收藏
-
- 文章 · 前端 | 6分钟前 |
- HTML文件怎么在浏览器打开?默认设置方法
- 402浏览 收藏
-
- 文章 · 前端 | 9分钟前 |
- HTML表格多语言支持方法有哪些?
- 263浏览 收藏
-
- 文章 · 前端 | 16分钟前 |
- JavaScriptfind方法使用教程
- 449浏览 收藏
-
- 文章 · 前端 | 26分钟前 |
- CSS中em是什么?em单位详解
- 480浏览 收藏
-
- 文章 · 前端 | 31分钟前 |
- HTML表格3D效果实现方法详解
- 368浏览 收藏
-
- 文章 · 前端 | 31分钟前 | 动画效果 text-shadow @keyframes CSS文字霓虹灯 颜色循环
- CSS实现文字霓虹灯效果教程
- 372浏览 收藏
-
- 文章 · 前端 | 33分钟前 |
- TypeScript泛型提升复用与类型安全
- 309浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 511次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 498次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- 千音漫语
- 千音漫语,北京熠声科技倾力打造的智能声音创作助手,提供AI配音、音视频翻译、语音识别、声音克隆等强大功能,助力有声书制作、视频创作、教育培训等领域,官网:https://qianyin123.com
- 119次使用
-
- MiniWork
- MiniWork是一款智能高效的AI工具平台,专为提升工作与学习效率而设计。整合文本处理、图像生成、营销策划及运营管理等多元AI工具,提供精准智能解决方案,让复杂工作简单高效。
- 118次使用
-
- NoCode
- NoCode (nocode.cn)是领先的无代码开发平台,通过拖放、AI对话等简单操作,助您快速创建各类应用、网站与管理系统。无需编程知识,轻松实现个人生活、商业经营、企业管理多场景需求,大幅降低开发门槛,高效低成本。
- 131次使用
-
- 达医智影
- 达医智影,阿里巴巴达摩院医疗AI创新力作。全球率先利用平扫CT实现“一扫多筛”,仅一次CT扫描即可高效识别多种癌症、急症及慢病,为疾病早期发现提供智能、精准的AI影像早筛解决方案。
- 126次使用
-
- 智慧芽Eureka
- 智慧芽Eureka,专为技术创新打造的AI Agent平台。深度理解专利、研发、生物医药、材料、科创等复杂场景,通过专家级AI Agent精准执行任务,智能化工作流解放70%生产力,让您专注核心创新。
- 128次使用
-
- 优化用户界面体验的秘密武器:CSS开发项目经验大揭秘
- 2023-11-03 501浏览
-
- 使用微信小程序实现图片轮播特效
- 2023-11-21 501浏览
-
- 解析sessionStorage的存储能力与限制
- 2024-01-11 501浏览
-
- 探索冒泡活动对于团队合作的推动力
- 2024-01-13 501浏览
-
- UI设计中为何选择绝对定位的智慧之道
- 2024-02-03 501浏览