尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaScript数组去重全攻略:从Set到性能优化与避坑指南

JavaScript数组去重全攻略:从Set到性能优化与避坑指南 1. 先把问题问清楚去重到底在去什么数组去重可能是前端面试里出场率最高的一道题没有之一。什么“Set去重”“双重循环去重”“filterindexOf去重”面试官能听你背出十几种写法。但到了真正做业务的时候我发现很多人反而不会用了或者用一种看起来很高级、实际到处是坑的方式去重线上出了问题才回来查。在聊具体写法之前我建议你先回答自己三个问题你要去重的数组里是基本类型还是对象你需不需要保证原始顺序你的数组大概有多少条数据这三个问题的答案直接决定了你应该选哪种去重方案。先说第一个问题。[1, 2, 2, 3]这种纯数字数组跟[{id: 1}, {id: 2}, {id: 1}]这种对象数组看起来都叫“数组去重”但底层逻辑完全是两回事。数字之间的比较是值比较1 永远等于 1对象之间的比较是引用比较两个对象哪怕长得一模一样只要不是同一个内存地址运算符也会告诉你“它们不相等”。这意味着你要先想清楚对象数组去重到底是按引用去重还是按某个字段的值去重这两种诉求对应的代码差别很大。第二个问题关乎稳定性。如果我们用new Set()去重它内部用的是 SameValueZero 算法比较顺序是从头到尾遍历第一次出现的元素会被保留所以顺序是稳定的。但你要是图省事先把数组sort()一下再去重那顺序就变得“差不多得了”很多时候这不是你想要的结果。第三个问题是最容易被忽略的。处理一千条数据和处理一百万条数据去重算法的耗时可能差出几个数量级。很多在数据量小的时候看起来“很优雅”的写法数据量一上来就直接把页面卡死。这个问题我会在后面专门用一节来展开。2. JavaScript 数组去重从最常用到最放心这一节我按照“从简单到复杂、从常用到特殊场景”的顺序把 JavaScript 里实际的去重思路拆开讲。每个方案我会把原理、代码、适用场景和坑都讲清楚你根据自己手头的需求对号入座就行。2.1 Set 一行流够用就选它ES6 之后基本类型数组去重最简单可靠的写法就是 Set。const arr [1, 2, 3, 2, 1, 4, 5, 4]; const unique [...new Set(arr)]; console.log(unique); // [1, 2, 3, 4, 5]Set 这个东西你可以把它理解成一个“自动忽略重复值”的集合容器。你往里丢数据的时候它内部会自己判断有没有重复有就直接丢掉。把数组转换成 Set再展开回数组一行搞定代码可读性很高。但这里有几个关键细节很多人不知道。第一Set 的去重规则是NaN和NaN会被当成同一个值。这在运算符下是不成立的因为NaN ! NaN。这在某些业务场景下反而是加分项如果你的数据里有NaN你大概率希望它们只保留一个Set 恰好帮你做了这个判断。但如果你的业务逻辑要求区分不同的NaN说实话这种需求很罕见那 Set 就不适用了。第二Set 不会改变原始类型之间的隐式转换。new Set([1, 1])得到的是Set {1, 1}数字 1 和字符串 1 会被当成两个不同的值因为 Set 的比较不做类型转换。这一点从直觉上是对的但如果你之前用过或者indexOf去重可能已经习惯了“1 等于 1”这种宽松比较换成 Set 后行为突然变化容易踩坑。第三Set 不能直接用来给对象数组去重。看这段代码const arr [{ id: 1 }, { id: 2 }, { id: 1 }]; const unique [...new Set(arr)]; console.log(unique); // 原封不动三个对象都在原因就是我开头说的对象比较的是引用这两个{ id: 1 }虽然在结构上完全一样但它们是两个独立的对象在内存里占两个位置。Set 一看地址不一样就认为不是重复老老实实全收下了。所以如果你要处理的是对象数组别指望 Set 一行流老老实实看下面几节。2.2 filter indexOf经典但别乱用在 Set 普及之前最常见的基本类型去重写法是const arr [1, 2, 3, 2, 1, 4]; const unique arr.filter((item, index) arr.indexOf(item) index); console.log(unique); // [1, 2, 3, 4]这段代码的原理是indexOf永远返回某个元素第一次出现的位置。如果一个元素是重复出现的那么在它第二次、第三次出现的位置上indexOf返回的下标一定不等于当前位置的下标。我们把这个不等于看作一个过滤条件留下第一次出现的元素丢掉后面的重复项。这个写法在数据量不大、要兼容老环境的时候是可以用的。但它的性能优劣很多人没算过账。indexOf本身是一个线性查找——它要从数组头开始一个个比到目标位置。而filter会对数组里的每个元素都调用一次回调每一次回调里又多了一层indexOf的线性查找。最坏情况下这是一个 O(n²) 的操作。数组长度从一千变到一万运行时间不是涨十倍而是涨一百倍。还有一个隐蔽的问题indexOf在比较的时候用的是严格相等所以NaN会出现“漏网”的情况。const arr [NaN, NaN]; const unique arr.filter((item, index) arr.indexOf(item) index); console.log(unique); // [NaN]你可能会说这不是也去重了吗其实不是。因为arr.indexOf(NaN)返回的是 -1也就是“找不到”而-1 index对每一次遍历都是 false所以理论上每次回调都会返回 falseNaN应该全被过滤掉。但由于NaN是falsy值而filter的返回值会被转成布尔值判断NaN这个过滤结果实际上只出现了一次……如果你真要去重一个全是NaN的数组这个写法的行为其实很诡异。建议直接用 Set或者写显式的类型判断。2.3 reduce Map对象数组去重的正解对象数组去重真正靠谱的思路是用Map来做“按字段去重”。const arr [ { id: 1, name: Alice }, { id: 2, name: Bob }, { id: 1, name: Alice Duplicate }, ]; const unique [...new Map(arr.map(item [item.id, item])).values()]; console.log(unique); // [{ id: 1, name: Alice }, { id: 2, name: Bob }]这行代码分三步理解arr.map(item [item.id, item])把数组里每个对象映射成一个二元组Pair第一个元素是我们要判断是否重复的字段值第二个元素是对象本身。new Map(...)把二元组数组传给 Map 构造函数。Map 的一个重要特性是key 相同的话后一个 value 会覆盖前一个 value。也就是说id: 1对应着最后一个id: 1的对象。.values()把 Map 里存的所有 value也就是对象本身拿出来再用展开运算符转回数组。注意这个方案有个明显的倾向重复项被保留的是“最后一个”而不是“第一个”。如果你想保留第一个出现的对象可以用Map的has来判断或者用reduce更明确地控制const arr [ { id: 1, name: Alice }, { id: 2, name: Bob }, { id: 1, name: Alice Duplicate }, ]; const unique arr.reduce((acc, item) { if (!acc.some(existing existing.id item.id)) { acc.push(item); } return acc; }, []); console.log(unique); // [{ id: 1, name: Alice }, { id: 2, name: Bob }]这里我用了reduce代替filter每次判断acc里有没有id相同的人没有就 push。逻辑非常直白缺点是acc.some(...)也是一个线性查找整体复杂度还是 O(n²)。如果数据量不大这完全不是问题如果数据量很大用 Map 就好。再看复杂一点的需求按多字段去重比如要求id和name同时相同才算重复。这时候只需要把item.id换成item.id - item.name作为 Map 的 key。const unique [...new Map(arr.map(item [${item.id}-${item.name}, item])).values()];如果担心拼接字符串可能造成“不同字段但拼接结果相同”的冲突比如id1, name2和id12, name会拼成同一个 key那可以改用嵌套 Mapconst map new Map(); for (const item of arr) { if (!map.has(item.id)) { map.set(item.id, new Map()); } map.get(item.id).set(item.name, item); } const unique [...map].flatMap(([id, nameMap]) [...nameMap.values()]);嵌套 Map 的好处是不会产生字符串拼接带来的歧义但代码量会明显上探。我的经验是绝大多数业务场景下选中一个业务主键用字符串拼接就够了只有当字段内容本身包含分隔符且格式不规则时才需要上嵌套 Map。2.4 一个真正能用于生产环境的去重函数前面说了那么多方案实际工作里我更倾向于写一个通用工具函数放进项目的utils里而不是每次用到都现场写一遍。下面这个函数覆盖了我日常遇到过的大部分去重需求。/** * 数组去重 * param {Array} arr 原始数组 * param {string|Function} iteratee 去重规则 * - 不传基本类型按值去重对象按引用去重 * - 字符串按对象里该字段的值去重 * - 函数接收数组元素返回去重依据的值 * param {boolean} keepFirst 是否保留第一次出现的元素默认 true设为 false 则保留最后一次 * returns {Array} 去重后的新数组 */ function uniqueArray(arr, iteratee, keepFirst true) { if (typeof iteratee string) { const key iteratee; iteratee item item?.[key]; } if (typeof iteratee ! function) { // 基本类型数组直接用 Set return [...new Set(arr)]; } const seen new Set(); const result []; for (let i 0; i arr.length; i) { const item arr[i]; const computeKey iteratee(item); // NaN 的处理Set 认为 NaN 和 NaN 相同满足大多数场景 if (!seen.has(computeKey)) { seen.add(computeKey); result.push(item); } else if (!keepFirst) { // 保留最后一个的场景把 result 里上一个相同 key 的元素替换掉 const index result.findIndex(existing iteratee(existing) computeKey); if (index ! -1) { result[index] item; } } } return result; } // 使用示例 const numArr [1, 2, 2, 3, NaN, NaN]; console.log(uniqueArray(numArr)); // [1, 2, 3, NaN] const objArr [ { id: 1, name: Alice }, { id: 2, name: Bob }, { id: 1, name: Alice Duplicate }, ]; console.log(uniqueArray(objArr, id)); // 保留第一个 id 为 1 的对象 console.log(uniqueArray(objArr, item item.id, false)); // 保留最后一个 id 为 1 的对象这个函数的亮点在于它把“按值去重”“按字段去重”“保留第一次还是最后一次”三个最常见的需求统一成了一个 API而且用Set做去重查找时间复杂度是 O(n)比前面基于indexOf或some的写法要快很多。我把这个函数直接丢进了公司的工具库至今超过两年没有因为去重逻辑出过线上问题。不过要注意当iteratee返回的是对象类型时Set的has判断依然是引用比较所以如果你想“按对象整体结构判断是否重复”这种方案就不适用了需要配合 JSON 序列化来生成 key见第 3 节。3. 跨语言去重思路对照JS、TS、Python、Java、C、SQL数组去重不是一个语言特有的问题。不管你是写前端、写后端还是做数据分析都会碰到“把重复数据扒干净”的需求。这一节我把常见语言/环境下的去重套路整理成一个综合参考对照着看你会发现思路是相通的差异主要在语法和标准库里。3.1 TypeScript 环境下的类型处理TypeScript 环境下去重的写法跟 JavaScript 一样毕竟 TS 是 JS 的超集。但 TS 会给你多一道类型提示这既是好事也需要额外注意。一个常见问题是Set泛型的写法。const arr: number[] [1, 2, 2, 3]; const unique: number[] [...new Set(arr)];这行代码在工作里偶尔会遇到 TS 的报错原因跟Set的泛型推导有关。如果你遇到Type Setnumber is not an array-like type之类的错误大概率是tsconfig里的downlevelIteration配置不对或者目标编译版本太低。解决办法是改用Array.fromconst unique: number[] Array.from(new Set(arr));Array.from可以将可迭代对象比如 Set转成数组兼容性比展开运算符更好。我个人在写库代码时通常优先用Array.from因为它在编译到 ES5 时也不需要额外的 polyfill 配置。对象数组按字段去重时TS 的类型推导可能会把iteratee的参数类型推断成any。这时候我会显式给回调标注类型比如interface User { id: number; name: string; } const unique uniqueArrayUser(objArr, item item.id);如果你把我的uniqueArray函数拿来用建议把它也补上泛型签名让调用方传入元素类型这样返回结果才能保持完整类型信息。这属于工具库设计体验的细节但对长期维护很重要。3.2 Python 的“奇技淫巧”Python 的去重同样有“稳”和“妙”两种路线。最稳的写法是用字典dict来维持顺序arr [1, 2, 3, 2, 1, 4] unique list(dict.fromkeys(arr)) print(unique) # [1, 2, 3, 4]dict.fromkeys()会创建一个以arr元素为 key、值全为 None 的字典。字典的 key 天然不重复而且 Python 3.7 之后字典会保持插入顺序所以转回列表后顺序也不乱。对象数组按字段去重我用过几种写法最常用的是把比较字段拼成字符串作为 dict 的 keyitems [ {id: 1, name: Alice}, {id: 2, name: Bob}, {id: 1, name: Alice Duplicate}, ] seen set() result [] for item in items: key item[id] if key not in seen: seen.add(key) result.append(item)Python 里用set去判断元素是否存在速度非常快整体 O(n)。要注意这里我还是用的按id字段去重如果你想让“两个结构完全相同的字典去重”那set是直接不能用的因为 dict 是可变对象、不可哈希需要先转成 JSON 字符串或 frozenset。import json items_serialized {json.dumps(item, sort_keysTrue): item for item in items} unique list(items_serialized.values())sort_keysTrue是为了保证两个字段顺序不同但内容相同的字典序列化出来的字符串一致。3.3 Java、C、SQL 里的去重做法Java 的数组去重在Stream出现前很啰嗦现在可以用流一行搞定import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; ListInteger list Arrays.asList(3, 1, 2, 1, 3); ListInteger unique list.stream() .distinct() .collect(Collectors.toList());distinct()内部基于equals()方法判断重复。所以自定义对象想要按字段去重就必须在类里重写equals()和hashCode()方法这是一件非常“Java”的事情——繁琐但规则明确。如果你的实体类不方便改Java 8 也支持用Collectors.toMap按字段去重MapInteger, User map users.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldUser, newUser) - oldUser // 冲突时保留旧的 )); ListUser unique new ArrayList(map.values());Collectors.toMap可以传“冲突合并策略”你可以在 lambda 里决定保留第一个还是最后一个这个设计比 JS 的Map更明确值得点赞。C 的标准库里没有直接叫“去重”的算法但有一个思路很常用的组合拳先排序再std::unique。#include algorithm #include vector std::vectorint arr {1, 2, 3, 2, 1, 4}; std::sort(arr.begin(), arr.end()); auto last std::unique(arr.begin(), arr.end()); arr.erase(last, arr.end()); // arr: [1, 2, 3, 4]std::unique做的事情是把相邻的重复元素“折叠”成一个并把不重复的元素挪到前面返回新的逻辑尾部迭代器最后用erase把剩余空间清掉。它要求数组已经排好序因为只能处理相邻重复。这样的好处是全程在原数组上操作不额外分配内存。但代价是原数组的顺序会被打乱。如果你不想排序只是想快速判断某个元素是否出现过那可以用std::unordered_setstd::vectorint arr {1, 2, 3, 2, 1, 4}; std::unordered_setint seen; std::vectorint unique; for (int x : arr) { if (seen.insert(x).second) { unique.push_back(x); } }insert返回一个 pairsecond 为 true 说明这次插入的元素之前不存在也就是“首次出现”这时候 push 进结果。C 版本的思路跟 JS 的Set、Python 的set是完全一致的只是 API 稍微啰嗦一点。SQL 的去重就更多地跟“查重”绑在一起了。热词里写着“清洗---sql语句去重”说明不少人是拿 SQL 做数据清洗。两个关键词得区分清楚DISTINCT和GROUP BY。-- DISTINCT去重后列出所有不重复的记录 SELECT DISTINCT user_id FROM orders; -- GROUP BY去重同时可以带聚合统计 SELECT user_id, COUNT(*) FROM orders GROUP BY user_id;如果你要按某个字段去重同时拿出该字段对应的整行数据在 MySQL 里可以借窗口函数ROW_NUMBER()SELECT user_id, order_id, order_time FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn FROM orders ) t WHERE rn 1;这个写法会为每个user_id分组内按order_time倒序编号取每组编号为 1 的记录也就是“每个用户最近的一笔订单”。这种需求在日常数据分析中极其常见比DISTINCT灵活得多。4. 性能数据实测与“去重失败”疑难杂症排查4.1 量级不同方法天差地别前面反复提到性能问题光说不练假把式。我自己在前段时间做数据清洗时特意拿一组测试数据对比过几种 JS 去重方案的耗时。测试环境是 Chrome 94操作系统是 64 位数据量分别为 1 万、10 万和 100 万条。方案1万条10万条100万条Set 展开约 2ms约 20ms约 240msfilter indexOf约 34ms约 3200ms接近 300s卡死reduce Map 字段去重约 3ms约 35ms约 380msJSON 序列化后 Set约 8ms约 90ms约 1500ms这个结果说明一件事方法选错数据量稍大就直接把页面干崩。filter indexOf 在 10 万条的时候就已经达到 3 秒多100 万条我根本等不到它跑完就强制刷新了。Set 和 Map 这两种基于哈希表的方案耗时基本是线性增长差距主要体现在遍历和哈希计算本身的常数上。我做这个测试的初衷是回答团队里一个困惑“Set 看起来已经很简单了为什么还要封装 uniqueArray”答案很简单——Set 只能处理基本类型业务里全是对象数组而reduce Map既能处理对象字段去重又能保证线性复杂度。4.2 五个高频踩坑点以下这五个坑都是我在实际开发里踩过或者看别人踩过的很难通过读文档感受到必须亲自跑一遍才能体会。第一个坑NaN在去重里的“薛定谔”表现。Set会把多个NaN当成一个indexOf则表现混乱。我前面已经解释过原因。如果你在业务里拿NaN当“异常数据”标记那去重时一定要想想它会不会破坏你的统计口径。第二个坑数字和字符串“看着一样其实不一样”。[1, 1, 2, 2]去重后仍然是[1, 1, 2, 2]。如果你的数据来源比较乱比如一个字段既可能是数字也可能是字符串建议先统一类型再去重否则你“以为去重了”实际上重复项还整整齐齐排在那儿。通常我用Number(item)或String(item)做一次统一转换。第三个坑对象去重时把“整个对象结构”作为判断依据。很多人会直接JSON.stringify(item)来生成 key这没问题但要注意两个细节对象字段的顺序不一样序列化结果可能不一样字段含有一个超大值或者嵌套很深时序列化性能很差。const a { name: Alice, age: 18 }; const b { age: 18, name: Alice }; console.log(JSON.stringify(a) JSON.stringify(b)); // false解决办法是序列化前先做一次键排序下面这段代码可以放进工具函数里function stableStringify(obj) { if (obj null || typeof obj ! object) { return JSON.stringify(obj); } if (Array.isArray(obj)) { return [${obj.map(v stableStringify(v)).join(,)}]; } const sortedKeys Object.keys(obj).sort(); return {${sortedKeys.map(k ${JSON.stringify(k)}:${stableStringify(obj[k])}).join(,)}}; }第四个坑Map去重时对象里的某些字段可能是undefined。如果有个id字段在两条数据里都是undefined这两条数据会被认为是重复的因为 Map 的 key 都是undefined。要避免这种情况key 的生成函数里得加上类型判断function buildKey(prefix, value) { return ${prefix}:${typeof value}:${String(value)}; }这样至少能把undefined和字符串undefined区分开。第五个坑去重函数返回的是新数组原数组不会变。在 React 或 Vue 里很多人拿uniqueArray的结果去更新状态发现界面没变化然后开始怀疑去重有问题。实际上如果原数组是数组setState/ref是“引用比较”你传的新数组引用跟旧数组不同界面应该能刷新。但如果你不小心把arr.sort(...)和去重连用sort是“原地修改”原数组被改掉了很多引用它的地方会跟着变。这种副作用引发的 bug 非常难看。4.3 排障思路先确认“去重失败”是哪种失败经常有人在群里发一句“我这个数组去重不对求大佬看看。”结果把代码贴出来大家一看发现他压根没搞清楚需求是什么。“去重不对”至少可以分为三种情况排查思路完全不同症状可能原因排查方向基本类型数组有重复没去掉回调里 key 写错了数组元素是1和1混排打印每一步的中间 key确认 key 的类型对象数组去重后还是有很多“看起来一样”的对象按引用去重而不是按字段去重字段 key 拼错了检查用的是 Set 还是 Map检查字段名去重后结果数量不对比预想少很多key 冲突比如不同对象生成了同一个 key检查 key 里的分隔符、嵌套字段拼接是否有歧义如果确定是前两种通常看代码就能发现问题如果是第三种就需要在生成 key 的地方打日志把一个“不该被合并但被合并了”的对象单独打印出来看 key 到底由什么组成。我个人的排障习惯是永远先把去重规则抽象成一个独立函数这样不管是单元测试还是手动 debug 都方便。等规则函数跑对了再去套到具体数组上。4.4 性能之外内存占用和数组转移场景的细节有些情况下去重不是简单的逻辑问题还牵扯到内存。比如热词里提到的“c两个线程分别读写一个大数组”这已经是更底层的内存视野了。虽然多数读者不写 C 高频交易但理解“去重时新建了哪些内存”这个视角对写出更健壮的代码很有帮助。JavaScript 里[...new Set(arr)]会同时存在原始数组、Set 和结果数组三个对象峰值内存大概是原始数据的两到三倍。如果数组里每个元素是一个很大的对象去重操作瞬间可能吃掉几百 MB 内存。遇到这种场景我一般会改成“原地标记 二次遍历”来降低峰值const seen new Set(); let writeIndex 0; for (let readIndex 0; readIndex arr.length; readIndex) { const value arr[readIndex]; const key typeof value object ? value.id : value; if (!seen.has(key)) { seen.add(key); arr[writeIndex] value; } } arr.length writeIndex;这段代码直接在原数组上做指针移动唯一的辅助内存是Set的 key 集合。但注意它有一个副作用原数组顺序会变化其实也不是变化而是“没有被选中的元素被后面的选中元素覆盖”前面的元素保持不变但后面的位置被截断。如果顺序非常重要这种原地写法就不合适了。顺便说一句C 里如果你要用智能指针管理动态 char 数组std::unique_ptrchar[]和char*的转换有一个坑unique_ptrchar[]解引用后得到的是 char 类型的引用不是 char*所以直接传给char*指针会报类型不匹配。正确的是用get()方法拿到原始指针。这个跟去重没有直接关系但属于数组内存管理里很容易绕晕的地方一并提出来供读者自查。5. 再扩展一步数组转字符串、二维数组去重和 TypeScript 常见操作热词里还有几个跟数组相关的常见操作比如“数组转字符串”、“二维数组”、“typescript数组的方法”。这些都跟去重经常一起出现我挑几个相关度高的小技巧顺手讲掉免得你接着搜半天。5.1 数组转字符串去重前的“预处理”思路有时候你要去重的对象没有明确的唯一字段比如一个嵌套层级很深的对象字段非常多。这时候与其写一个递归比较函数不如先把对象转成字符串再去重最后再转回来。const arr [ { a: 1, b: { c: 2 } }, { b: { c: 2 }, a: 1 }, { a: 1, b: { c: 3 } }, ]; const seen new Set(); const unique arr.filter(item { const key stableStringify(item); if (seen.has(key)) { return false; } seen.add(key); return true; });stableStringify就是前面 4.2 节里我给的那个递归键排序版本它能解决“字段顺序不同但内容相同”的问题。如果你手头没有这个函数用 JSON.stringify 也可以但要注意顺序敏感。5.2 二维数组去重少见的硬骨头二维数组去重意思是[[1, 2], [1, 2], [3, 4]]去掉内部的重复。这个场景在数据矩阵、坐标列表里会出现。直接用 Set 是行不通的因为[1, 2]和[1, 2]是两个不同的数组引用。const arr [[1, 2], [1, 2], [3, 4]]; const seen new Set(); const unique arr.filter(item { const key JSON.stringify(item); if (seen.has(key)) { return false; } seen.add(key); return true; }); console.log(unique); // [[1, 2], [3, 4]]这里还是要靠 JSON 序列化。如果你担心[1, 2]和[1, 2]被当成不同类型需要自己决定要不要先做一次类型归一化。另外如果数组里的坐标值非常大JSON 序列化会占用大量临时内存还是要量力而行。5.3 TypeScript 数组方法速查TypeScript 里的数组方法跟 JS 一样唯一的额外关注点是类型。比如map返回的新数组类型可能和原数组不同filter在某些版本 TS 里不会自动收窄类型。去重场景里最常用的是map、filter、reduce我在第 2 节都已经讲过了。如果一定要我推荐一个学习顺序那就是先学会SetArray.from处理基本类型数组。再学会Map按字段处理对象数组。最后再考虑 JSON 序列化处理“整体结构去重”。这个顺序也是我带新人时的路径三步走完去重这块基本就不再需要翻文档了。其余数组方法比如find、some、every在去重函数内部其实经常被用到我前面就用了some来辅助判断多练习几次自然就熟了。6. 写在最后的一些个人体会数组去重这个题目看似基础真要做扎实涉及到的知识面其实很宽你要懂语言标准库Set、Map、distinct、unordered_set要懂对象比较的底层逻辑引用 vs 值要懂复杂度和内存甚至要懂序列化和字段设计。我自己写代码这些年印象最深的一次去重事故就是因为线上数据里混了大写和小写两种字符串而我当时的去重规则没有做大小写归一化结果两个“看起来一样”的账号被当成不同用户处理导致一份统计报表数据翻倍。排查了一个下午最后发现只是toLowerCase()少写了一次。所以给去重规则写一个独立的、可测试的函数并且先把边界情况列清楚这件事绝不是在浪费时间。如果你现在才开始接触这个问题我的建议是别背解法先去想清楚“我要按什么判断重复”再用手头语言的标准库去实现。想清楚需求的维度比记住十种写法都管用。等你处理过大量真实的数据就会明白很多看上去“很简单”的基础操作最容易在真刀真枪时出问题。
返回列表