全网最全情景,深入浅出解析JavaScript数组去重:数值与引用类型的全面攻略

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 如果是基础类型数组,优先选择 Set。 对于引用类型数组,根据需求选择 Map 或 JSON.stringify()。 其余情况根据实际需求进行混合调用,就能更好的实现数组去重。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~


作者:watermelo37

涉及领域:Vue、SpingBoot、Docker、LLM、python等

---------------------------------------------------------------------

温柔地对待温柔的人,包容的三观就是最大的温柔。

---------------------------------------------------------------------

image.gif 编辑

深入浅出解析JavaScript数组去重:数值与引用类型的全面攻略

image.gif 编辑

一、引言:我们为什么需要关注数组去重?

       在日常开发中,数组去重是一个不可避免的话题。不管是简单的数值数组去重,还是复杂的引用类型数组去重,掌握多种方法可以帮助开发者高效、优雅地解决实际问题。在这篇博客中,我们将从基础到进阶,结合大量代码案例,系统介绍数组去重的各种技巧。

前排提醒:

       本文主要讨论的去重情况包括:

       1、数值类去重,数组的元素往往是基础数据类型,这种情况相对容易,有很多种方法可以做到。

       2、引用类去重,数组的元素往往是对象、数组甚至多类型混合。这种情况下的去重会复杂很多,并且还可以分为去除完全重复和部分重复两种。

       ①去除完全重复就是两个引用类型的元素完全一致,其中还包括元素内容一致但键值对顺序不一致的特殊情况。

       ②去除部分重复就比如两个元素的name一致,就要去除其中的一个,并根据某种规则留下一个特定的元素。

二、数值类去重

1、使用 Set 去重

       Set 是去重的“万金油”,可以自动移除重复的元素,适合大多数基础类型数组去重。简洁高效,推荐使用。但仅适合基础数据类型。

let numbers = [1, 2, 2, 3, 4, 4, 5];
let uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers);
// 输出:[1, 2, 3, 4, 5]

image.gif

2、遍历 + includes()

       对于初学者,遍历结合 includes() 是一种直观的方法,易于理解,适合新手学习,但对大数组性能较低。初学者做开发学习时候遇不到大数组大数据,所以这个方法是完全可行的。

let numbers = [1, 2, 2, 3, 4, 4, 5];
let uniqueNumbers = [];
for (let num of numbers) {
    if (!uniqueNumbers.includes(num)) {
        uniqueNumbers.push(num);
    }
}
console.log(uniqueNumbers);
// 输出:[1, 2, 3, 4, 5]

image.gif

3、使用 filter() 和 indexOf()

       filter() 和 indexOf() 是函数式编程的经典组合,用于去重非常直观,写法简洁,可作为 Set 的替代方案,但是indexOf() 遍历时间复杂度较高。

let numbers = [1, 2, 2, 3, 4, 4, 5];
let uniqueNumbers = numbers.filter((num, index) => numbers.indexOf(num) === index);
console.log(uniqueNumbers);
// 输出:[1, 2, 3, 4, 5]

image.gif

4、使用 reduce()

       reduce() 可以通过累积器动态生成去重数组,灵活且功能强大,灵活性强,可结合复杂逻辑处理,但代码相对复杂。

       这里使用reduce()核心是利用它的累加器,累加器不只可以用来累加,可以用来做任何事情,包括特殊情况下作为forEach()和map()的替代。

let numbers = [1, 2, 2, 3, 4, 4, 5];
let uniqueNumbers = numbers.reduce((acc, num) => {
    if (!acc.includes(num)) {
        acc.push(num);
    }
    return acc;
}, []);
console.log(uniqueNumbers);
// 输出:[1, 2, 3, 4, 5]

image.gif

5、嵌套数组去重:结合 flat()

       如果数组中有嵌套的情况,可以配合 flat() 进行去重。

let nestedNumbers = [1, [2, 2], [3, 4, 4], 5];
let uniqueNumbers = [...new Set(nestedNumbers.flat())];
console.log(uniqueNumbers);
// 输出:[1, 2, 3, 4, 5]

image.gif

三、引用类去重——去除完全重复的对象元素

       引用类去重的场景更为复杂,因为对象和数组属于引用类型,哪怕键值完全相同也会分属于不同的引用地址,直接比较无法判断重复性。我们会从简单到复杂,详细介绍解决方案。

       完全重复是指两个对象的键值对完全相同。

1、JSON.stringify() + Set

       将对象转换为字符串表示,再利用 Set 去重,这样代码简洁,适合结构简单的对象数组,但是对嵌套对象或顺序无关的对象有局限性(比如某个相同元素name在前,id在后,这样就无法去重了)。

let objects = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { id: 1, name: "Alice" }
];
let uniqueObjects = Array.from(new Set(objects.map(JSON.stringify))).map(JSON.parse);
console.log(uniqueObjects);
// 输出:[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

image.gif

2、使用 Map() 方法

       通过 Map 的键值对特性保存唯一对象,适合复杂数据结构,性能优于 Set。

let objects = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { id: 1, name: "Alice" }
];
let uniqueObjects = Array.from(new Map(objects.map(obj => [JSON.stringify(obj), obj])).values());
console.log(uniqueObjects);
// 输出:[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

image.gif

Tips:

       在JavaScript中,Map 和 Set 是两种不同的集合类型,它们都是ES6中引入的,用于存储和管理一系列值。它们之间有几个关键的区别:

Map 对象

  • Map 对象保存键值对,并且能够记住键的原始插入顺序。
  • 任何值(对象或者原始值)都可以作为一个键或一个值。
  • Map 对象提供了许多实用的方法,如 set、get、has 和 delete,来操作映射。
  • Map 对象是可迭代的,这意味着它们可以用于 for...of 循环。

Set 对象

  • Set 对象只保存唯一的值,即不允许重复。
  • Set 对象同样保存元素的插入顺序。
  • Set 对象提供了 add、has 和 delete 等方法来操作集合。
  • Set 对象也是可迭代的,适用于 for...of 循环。

       一言以蔽之,一个是键值对集合,一个是数值集合。

四、特殊情况:对象的键值对可能顺序不同,但其内容相同

       当对象的键值对顺序不同,但其内容相同时,使用 JSON.stringify() 或 Map() 方法会出现问题,因为 JSON.stringify() 会把对象的键值对顺序也纳入到字符串化的过程,而 Map() 是基于键值对的映射,在对象属性的顺序不同的情况下,结果也可能不一致。

       比如这样的数据就是无法去重的:

let objects = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { name: "Alice", id: 1 }
];

image.gif

        为了处理这类键顺序不同但内容相同的对象去重,我们可以采取以下两种方法:

1、自定义函数比较对象内容

       通过自定义函数来统一对象的键的顺序,确保不论顺序如何都能正确去重。

function deepEqual(obj1, obj2) {
    const keys1 = Object.keys(obj1).sort();
    const keys2 = Object.keys(obj2).sort();
    
    if (keys1.length !== keys2.length) {
        return false;
    }
    for (let i = 0; i < keys1.length; i++) {
        const key = keys1[i];
        if (obj1[key] !== obj2[key]) {
            return false;
        }
    }
    return true;
}
let objects = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { name: "Alice", id: 1 }
];
let uniqueObjects = [];
for (let obj of objects) {
    if (!uniqueObjects.some(existingObj => deepEqual(existingObj, obj))) {
        uniqueObjects.push(obj);
    }
}
console.log(uniqueObjects);
// 输出:[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

image.gif

2、标准化对象的键顺序

       另一个方法是将对象的键排序,确保对象在进行去重时顺序一致。可以使用 JSON.stringify() 对每个对象进行处理,但是要先标准化它们的键顺序,然后进行比较。

function sortObjectKeys(obj) {
    const sortedObj = {};
    Object.keys(obj).sort().forEach(key => {
        sortedObj[key] = obj[key];
    });
    return sortedObj;
}
let objects = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { name: "Alice", id: 1 }
];
// 将对象的键值对顺序标准化
let uniqueObjects = [...new Set(objects.map(obj => JSON.stringify(sortObjectKeys(obj))))].map(item => JSON.parse(item));
console.log(uniqueObjects);
// 输出:[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

image.gif

3、使用 Map 存储标准化后的对象

       可以将对象标准化为字符串后,存储在 Map 中以实现去重。

function sortObjectKeys(obj) {
    const sortedObj = {};
    Object.keys(obj).sort().forEach(key => {
        sortedObj[key] = obj[key];
    });
    return sortedObj;
}
let objects = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { name: "Alice", id: 1 }
];
let map = new Map();
objects.forEach(obj => {
    map.set(JSON.stringify(sortObjectKeys(obj)), obj);
});
let uniqueObjects = Array.from(map.values());
console.log(uniqueObjects);
// 输出:[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

image.gif

五、引用类去重——去除部分重复的对象元素

       比如根据id去重、根据name去重,这种情况往往是某些不会重复的字段因为数据库的迁徙、合并或者增删修改导致了重复,除了要进行去重以外,还要根据某种规则留下一个特定的元素(因为两个元素并不完全一致)

function sortObjectKeys(obj) {
    const sortedObj = {};
    Object.keys(obj).sort().forEach(key => {
        sortedObj[key] = obj[key];
    });
    return sortedObj;
}
let objects = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { name: "Alice", id: 1 }
];
// 将对象的键值对顺序标准化
let uniqueObjects = [...new Set(objects.map(obj => JSON.stringify(sortObjectKeys(obj))))].map(item => JSON.parse(item));
console.log(uniqueObjects);
// 输出:[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

image.gif

1、保留每个 id 对应的最后一个对象

let objects = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { id: 1, name: "Charlie" }
];
let uniqueById = Array.from(new Map(objects.map(obj => [obj.id, obj])).values());
console.log(uniqueById);
// 输出:[{ id: 2, name: 'Bob' }, { id: 1, name: 'Charlie' }]

image.gif

2、根据多字段组合去重

       这种往往是数据库迁徙、合并的时候出现的问题,新旧数据混合。

let objects = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { id: 1, name: "Alice", age: 25 },
    { id: 1, name: "Alice", age: 30 }
];
let uniqueByFields = objects.reduce((acc, obj) => {
    if (!acc.some(item => item.id === obj.id && item.name === obj.name)) {
        acc.push(obj);
    }
    return acc;
}, []);
console.log(uniqueByFields);
// 输出:[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

image.gif

3、根据特定规则保留对象:保留最高分的学生记录

       比如在多次测评中,某人获得了不同的成绩,要保留其最高分。

let students = [
    { id: 1, name: "Alice", score: 85 },
    { id: 2, name: "Bob", score: 90 },
    { id: 1, name: "Alice", score: 95 }
];
let highestScoreStudents = Array.from(
    students.reduce((acc, student) => {
        if (!acc.has(student.id) || acc.get(student.id).score < student.score) {
            acc.set(student.id, student);
        }
        return acc;
    }, new Map()).values()
);
console.log(highestScoreStudents);
// 输出:[{ id: 2, name: 'Bob', score: 90 }, { id: 1, name: 'Alice', score: 95 }]

image.gif

六、混合数组去重

       对于包含基础数据类型和引用类型的混合数组,可以分别处理后合并。

let mixedArray = [1, "a", { id: 1 }, "a", { id: 1 }, 2, 1];
// 分离基础类型和引用类型
let primitives = mixedArray.filter(item => typeof item !== "object");
let objects = mixedArray.filter(item => typeof item === "object");
// 分别去重后合并
let uniqueMixedArray = [
    ...new Set(primitives),
    ...Array.from(new Map(objects.map(obj => [JSON.stringify(obj), obj])).values())
];
console.log(uniqueMixedArray);
// 输出:[1, 'a', 2, { id: 1 }]

image.gif

七、对比与总结

1、使用场景对比

方法 适用场景 优点 缺点
Set 基础类型数组去重 简洁高效 无法处理引用类型
遍历 + includes 基础类型数组去重 易理解 性能较低
filter() + indexOf() 基础类型数组去重 通用 性能较低
reduce() 复杂逻辑处理或混合类型数组去重 灵活,可扩展逻辑 写法稍复杂
JSON.stringify 引用类型数组去重 简洁 无法处理嵌套或无序字段的对象
Map 引用类型数组去重 性能较优,适合复杂数据结构 写法稍繁琐

2、总结

       如果是基础类型数组,优先选择 Set。

       对于引用类型数组,根据需求选择 Map 或 JSON.stringify()。

       其余情况根据实际需求进行混合调用,就能更好的实现数组去重。

       只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

       其他热门文章,请关注:

       你真的会使用Vue3的onMounted钩子函数吗?Vue3中onMounted的用法详解

       极致的灵活度满足工程美学:用Vue Flow绘制一个完美流程图

       通过array.filter()实现数组的数据筛选、数据清洗和链式调用

       el-table实现动态数据的实时排序,一篇文章讲清楚elementui的表格排序功能

       TreeSize:免费的磁盘清理与管理神器,解决C盘爆满的燃眉之急

       Dockerfile全面指南:从基础到进阶,掌握容器化构建的核心工具

       在线编程实现!如何在Java后端通过DockerClient操作Docker生成python环境

       MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver

       JavaScript中闭包详解+举例,闭包的各种实践场景:高级技巧与实用指南

       干货含源码!如何用Java后端操作Docker(命令行篇)

       Idea启动SpringBoot程序报错:Port 8082 was already in use;端口冲突的原理与解决方案

       PDF预览:利用vue3-pdf-app实现前端PDF在线展示

相关文章
|
6月前
|
JavaScript 前端开发 Go
CSS 与 JS 对 DOM 解析和渲染的影响
【10月更文挑战第16天】CSS 和 JS 会在一定程度上影响 DOM 解析和渲染,了解它们之间的相互作用以及采取适当的优化措施是非常重要的。通过合理的布局和加载策略,可以提高网页的性能和用户体验,确保页面能够快速、流畅地呈现给用户。在实际开发中,要根据具体情况进行权衡和调整,以达到最佳的效果。
|
1月前
|
数据采集 前端开发 JavaScript
金融数据分析:解析JavaScript渲染的隐藏表格
本文详解了如何使用Python与Selenium结合代理IP技术,从金融网站(如东方财富网)抓取由JavaScript渲染的隐藏表格数据。内容涵盖环境搭建、代理配置、模拟用户行为、数据解析与分析等关键步骤。通过设置Cookie和User-Agent,突破反爬机制;借助Selenium等待页面渲染,精准定位动态数据。同时,提供了常见错误解决方案及延伸练习,帮助读者掌握金融数据采集的核心技能,为投资决策提供支持。注意规避动态加载、代理验证及元素定位等潜在陷阱,确保数据抓取高效稳定。
71 17
|
1月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
1月前
|
消息中间件 JavaScript 前端开发
最细最有条理解析:事件循环(消息循环)是什么?为什么JS需要异步
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。 本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。 但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗? 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您
|
6月前
|
存储 前端开发 JavaScript
JavaScript垃圾回收机制深度解析
【10月更文挑战第21】JavaScript垃圾回收机制深度解析
162 59
|
5月前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
170 17
|
5月前
|
存储 JavaScript 前端开发
js的基础类型和引用类型
【10月更文挑战第29天】理解 JavaScript 中的基础类型和引用类型的区别对于正确地编写代码和理解程序的行为非常重要。在实际开发中,需要根据具体的需求合理地选择和使用不同的数据类型,以避免出现一些意想不到的错误和问题。同时,在处理引用类型数据时,要特别注意对象的引用关系,避免因共享引用而导致的数据不一致等问题。
|
5月前
|
前端开发 JavaScript
JavaScript新纪元:ES6+特性深度解析与实战应用
【10月更文挑战第29天】本文深入解析ES6+的核心特性,包括箭头函数、模板字符串、解构赋值、Promise、模块化和类等,结合实战应用,展示如何利用这些新特性编写更加高效和优雅的代码。
133 0
|
1月前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
166 29
|
1月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
58 3

热门文章

最新文章

推荐镜像

更多
下一篇
oss创建bucket