ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaScript集合去重与排序:从Set到对象字段去重的实战指南

JavaScript集合去重与排序:从Set到对象字段去重的实战指南 很多前端人第一次接触“集合去重和排序”这两个操作都是从面试题开始或者从实际需求里被迫学会的。我以前也是遇到重复数据就[...new Set(arr)]遇到排序就arr.sort((a, b) a - b)以为这两行代码就是全部答案。直到后来在处理对象数组、字段合并、大数据量排序时接连踩坑才发现这里面的门道比想象中多得多。这篇文章把我在真实项目里积累的经验一次说清楚。全文围绕“去重”和“排序”这两件事从最基本的值类型数组到对象数组按字段去重再到多条件排序、稳定性处理、性能优化最后封装成可以直接抄走用的工具函数。不管你是刚入行的前端新人还是已经在写复杂中后台系统、天天跟表格数据打交道的开发者应该都能从这里找到有用的东西。1. 去重与排序的整体设计思路1.1 先想清楚你处理的是“值”还是“引用”很多人去重翻车根源在于没分清“基本类型值”和“引用类型对象”的区别。对于[1, 2, 2, 3, 3]这种数组里面的元素是基本类型number、string、boolean、null、undefined、symbol、bigint去重只需要比较值是否相等。但对于对象数组比如[{id: 1, name: a}, {id: 1, name: a}]这两个对象虽然内容一样但在内存里是两个不同的引用用new Set()直接去重是无效的因为 Set 判断重复用的是SameValueZero算法它只认引用是否相同不认内容是否相同。实际项目里我们更常遇到的是“根据对象某个唯一字段去重”比如用户列表按userId去重订单列表按orderNo去重。这种场景下不能用Set直接兜底需要自己维护一个“已经见过的 key”的映射表。1.2 明确排序的稳定性和效率需求排序算法本身就有十几种冒泡、选择、插入、希尔、归并、快排、堆排、计数、基数、桶排……但 JavaScript 的Array.prototype.sort()已经帮我们封装好了底层排序V8 引擎在数组长度小于等于 10 时使用插入排序大于 10 时使用快速排序现在新版 V8 用的是 TimSort 的变体它在大多数情况下是稳定排序。这里有一个关键点排序稳定性。如果你的数据先按时间排了一次又按城市排了一次而第二次排序是不稳定的那么同城市内部的相对顺序就会被打乱。JS 的sort()在现代引擎里是稳定的但如果你自己写快排稍不注意就会变成不稳定排序。所以在绝大多数业务场景下我建议直接使用内置sort()不要自己造排序轮子。1.3 组合思路先去重再排序还是先排序再去重有人会问这两个操作谁先谁后有区别吗有而且区别很大。如果先去重再去排序那么去重阶段只需要一个Set或Map时间复杂度 O(n)排序阶段用内置sort()时间复杂度 O(n log n)。整体复杂度是 O(n log n)。如果先排序再去重那么排序后相同的元素会相邻去重时可以只比较当前元素和前一个元素。这样去重逻辑会更简单但排序同样要花 O(n log n)。另外有个陷阱如果对象数组根据多个字段判断是否重复先排序再去重可能会因为排序字段和去重字段不一致而漏掉或错杀数据。所以我的建议是默认先去重再排序。这样思路清晰而且在大部分数据量场景下性能已经足够。只有当你的数据量达到几十万上百万级别且需要极致的性能优化时才需要去琢磨“先排序相邻去重”这种 O(n log n) 内完成的方案。2. 数值数组去重从暴力到优雅2.1 双重循环去重最朴素的方法就是两层循环外层遍历原数组内层遍历结果数组看当前元素是否已经在结果里出现过。function uniqueDup(arr) { const result []; for (let i 0; i arr.length; i) { let isDuplicate false; for (let j 0; j result.length; j) { if (arr[i] result[j]) { isDuplicate true; break; } } if (!isDuplicate) { result.push(arr[i]); } } return result; }这个方法的优点是兼容性极好代码逻辑直观适合教学和小数据量场景。缺点是时间复杂度是 O(n²)数据量一大就慢得离谱。比如 1 万个元素的数组最坏情况下要比较约 5000 万次浏览器分分钟卡顿。2.2 indexOf/includes 去重用indexOf或includes可以替代内层循环代码更简洁但本质上时间复杂度还是 O(n²)因为indexOf内部同样是线性查找。const arr [1, 2, 2, 3, 4, 4, 5]; const result arr.filter((item, index) arr.indexOf(item) index);这里filter遍历整个数组indexOf又从头查找所以每个元素都要往复扫描一遍。好处是写法很“函数式”可读性不错坏处是性能依然不理想而且对于NaN这类特殊值indexOf找不到indexOf用的是严格相等NaN ! NaN用includes则可以正确处理NaN。2.3 Set 去重现代 JavaScript 的默认答案ES6 引入Set之后去重变成了一行代码const uniqueArr [...new Set([1, 2, 2, 3, 3, 4])]; // [1, 2, 3, 4]Set内部使用哈希表实现插入和查找的平均时间复杂度都是 O(1)所以整体去重是 O(n)。这是目前处理基本类型数组去重最推荐的方式。这里要补充一个细节Set的判重规则是SameValueZero它把NaN视为相等。所以[NaN, NaN]用 Set 去重后是[NaN]而用indexOf去重做不到这一点。另外new Set([-0, 0])只保留一个 0因为SameValueZero认为-0和0是相等的。这些边界行为在实际开发中需要留意。2.4 保持首次出现顺序的技巧Set 去重天然保持元素的首次出现顺序因为 Set 底层链表和哈希表结合迭代顺序就是插入顺序。所以const arr [3, 1, 2, 1, 3, 4, 2]; const unique [...new Set(arr)]; // [3, 1, 2, 4]你会发现去重结果里 3 在 1 前面1 在 2 前面和原数组首次出现顺序一致。这在某些场景很重要比如保持用户选择的操作顺序或者保持后端返回的数据顺序。如果需求是“去重后还要按数值升序”那就直接[...new Set(arr)].sort((a, b) a - b)。2.5 大数据量下的性能实测我之前用 10 万个随机整数做过一次简单测试在本机 Chrome 里跑结果大概是这样方案耗时结论双重循环卡死几秒无响应不可用filter indexOf约 2000ms勉强可用但慢Set约 5ms强烈推荐这个量级差距非常直观。所以凡是数组元素是基本类型且数据量可能超过几百上千无脑用Set就好。不要觉得“反正数据量不大”接口哪天加个分页全量返回性能说崩就崩。3. 对象数组去重按字段去重的正确姿势3.1 为什么 Set 对对象数组无效看这个例子const list [ { id: 1, name: a }, { id: 1, name: a }, ]; console.log(new Set(list).size); // 2两个对象内容相同但 Set 认为它们不重复因为它们是两个不同的对象引用。这和基本类型的行为完全不一样。3.2 使用 Map 按唯一键去重最常见的做法是使用Map以对象的某个唯一标识字段作为 keyconst list [ { id: 1, name: a }, { id: 2, name: b }, { id: 1, name: a }, { id: 3, name: c }, ]; function uniqueByField(arr, key) { const map new Map(); for (const item of arr) { if (!map.has(item[key])) { map.set(item[key], item); } } return [...map.values()]; } const result uniqueByField(list, id); // [{id:1,name:a}, {id:2,name:b}, {id:3,name:c}]这里有几个细节值得注意第一Map的 key 可以是任意类型但如果我们用对象作为 key会因为对象引用不同而失效。所以一定要用基本类型字段作为 key比如字符串 id、数字 code 等。第二保留的是“第一次出现”的那一项因为我们在!map.has(item[key])时才set。如果你希望保留最后一次出现可以改成每次都map.set(item[key], item)这样最后 map 里保存的就是遍历过程中的最后一项。第三Map 的遍历顺序是插入顺序所以[...map.values()]返回的数组保持了每组重复项中第一次出现的顺序。3.3 更通用的多字段去重如果去重依据不是单个字段而是多个字段组合比如根据type id去重可以把多个字段拼成一个字符串作为 keyfunction uniqueByFields(arr, fields) { const map new Map(); for (const item of arr) { const key fields.map((f) item[f]).join(|); if (!map.has(key)) { map.set(key, item); } } return [...map.values()]; } const result uniqueByFields(list, [type, id]);拼接的时候要注意字段值本身如果包含分隔符|可能导致不同的组合拼出相同的 key。稳妥的做法是使用JSON.stringify(fields.map(...))或自定义一个稳定的序列化函数。但在大多数业务场景中用一个不太可能出现在业务数据里的分隔符就够了不必过度设计。3.4 全对象深比较去重什么时候需要有时候你需要比较整个对象的内容而不是某个唯一键。这通常出现在你需要合并两份可能存在嵌套结构的配置数据时。最简单粗暴的方案是把每个对象序列化为字符串再用 Set 去重function uniqueByDeep(objArr) { const seen new Set(); return objArr.filter((obj) { const key JSON.stringify(obj); if (seen.has(key)) { return false; } seen.add(key); return true; }); }但这里有一个大坑JSON.stringify对同一个对象如果键值顺序不同序列化结果会不同如果对象里有undefined值、函数、Symbol属性序列化时会丢失NaN会被序列化成null。所以这种方案适合“结构非常规整”的数据比如后端返回的标准化 JSON 配置而对包含多种类型、键顺序不稳定的对象容易误判。如果想要严谨的深度比较可以引入lodash的isEqual或者自己写递归比较。但说实话生产环境里真正需要“全对象深比较去重”的需求并不多见大部分场景都是“指定唯一键去重”所以不要一上来就上重型工具。4. 排序不只是sort((a,b)a-b)那么简单4.1 认识默认的字符串排序很多人第一次用sort()时会被坑到const arr [10, 9, 100, 2]; arr.sort(); // [10, 100, 2, 9]为什么因为默认的sort()会把元素转换成字符串按 Unicode 码点排序。所以“10”和“100”都排在“2”前面。想要按数值大小排序必须传入比较函数(a, b) a - b。const arr [10, 9, 100, 2]; arr.sort((a, b) a - b); // [2, 9, 10, 100]这是新手最容易踩的第一个排序坑。要理解的是比较函数返回负数、零、正数分别表示 a 应该排在 b 前面、两者相等、a 应该排在 b 后面。4.2 升序、降序、中文排序、混合排序升序arr.sort((a, b) a - b)。降序arr.sort((a, b) b - a)。字符串按字母顺序排const names [Bob, Alice, Carol]; names.sort((a, b) a.localeCompare(b)); // [Alice, Bob, Carol]中文按拼音排const chineseNames [张三, 李四, 王五]; chineseNames.sort((a, b) a.localeCompare(b, zh-Hans-CN));localeCompare可以用本地化规则处理中文拼音、声调等在中文业务场景里特别常用。比如城市列表按拼音排序用localeCompare就好。混合排序有时候你要先按一个字段排序再按另一个字段排序。可以这么写const users [ { name: 张三, age: 30 }, { name: 李四, age: 25 }, { name: 王五, age: 25 }, ]; users.sort((a, b) { if (a.age ! b.age) { return a.age - b.age; // 先按 age 升序 } return a.name.localeCompare(b.name); // 再按 name 升序 });这种方式非常直观也不容易出错。4.3 对象数组排序的常见写法实际业务中对对象数组排序的需求一般是按数值字段排序价格、数量、时间戳按日期字符串排序按多个字段组合排序例如楼层 房间号按自定义业务顺序排序例如状态待处理 处理中 已完成按时间字符串排序如果日期格式是YYYY-MM-DD HH:mm:ss可以直接用字符串比较const list [ { time: 2024-03-01 10:00:00 }, { time: 2024-02-28 12:30:00 }, ]; list.sort((a, b) a.time.localeCompare(b.time));如果是时间戳数字直接相减就行。按自定义业务顺序排序可以用一个映射表const statusOrder { pending: 0, processing: 1, completed: 2 }; const tasks [{ status: completed }, { status: pending }, { status: processing }]; tasks.sort((a, b) statusOrder[a.status] - statusOrder[b.status]);这种方法很实用比在比较函数里写一堆 if-else 要清爽得多。4.4 排序稳定性这是个一直被忽视的问题排序稳定性的意思是当两个元素的排序依据相等时它们原本的相对顺序在排序后不会改变。早期的 V8 快排不是稳定排序后来 V8 升级到 TimSort混合了归并和插入排序后sort()才稳定。现代浏览器基本都支持稳定排序但如果你自己实现排序算法或者在某些老环境里跑还是要注意。比如用户表格先按“注册时间”升序排好了然后用户点击“年龄”列希望年龄相同的人仍然保持注册时间的先后顺序。得益于稳定排序你只需按年龄排序即可不需要再把注册时间纳入排序条件。如果担心环境不稳定可以显式地把次要排序条件写进去users.sort((a, b) { if (a.age ! b.age) return a.age - b.age; return a.createdAt - b.createdAt; });这样最保险。4.5 大数据量排序的注意事项内置sort()用的是优化过的混合排序算法性能通常足够好。但有一个容易被忽略的问题排序是原地修改原数组的。const arr [3, 1, 2]; const sorted arr.sort((a, b) a - b); console.log(arr); // [1, 2, 3] console.log(sorted); // [1, 2, 3]如果你不想改动原数组需要先拷贝const newArr [...arr].sort((a, b) a - b);另外当数组规模非常大时几十万条以上排序本身会占用额外内存归并排序的特性但一般不用担心。真正的问题往往出现在排序回调函数写得过于复杂。比较函数会被调用很多次如果每次比较都做昂贵的计算比如正则匹配、深拷贝性能就会急剧下降。这时候可以考虑预处理把需要比较的字段提取出来建立索引数组再排序。5. 实战把去重和排序封装成可复用的工具函数5.1 一个完整的基础工具函数我在实际项目中会把这些逻辑封装成一个小模块放工具库里。下面是一个相对完整的基础版本/** * 数组按指定字段去重返回新数组不修改原数组 * param {Array} arr - 原数组 * param {string|string[]} key - 单个字段或字段数组 * param {boolean} keepLast - 是否保留最后一项默认保留第一项 * returns {Array} 去重后的新数组 */ function uniqueBy(arr, key, keepLast false) { const fields Array.isArray(key) ? key : [key]; const map new Map(); for (const item of arr) { const k fields.map((f) item[f]).join(__); if (keepLast) { map.set(k, item); } else if (!map.has(k)) { map.set(k, item); } } return [...map.values()]; } /** * 数组排序支持多个排序条件 * param {Array} arr - 原数组 * param {Array} sortKeys - 排序配置数组例如 [{ field: age, order: asc }, { field: name, order: desc }] * returns {Array} 排序后的新数组不修改原数组 */ function sortBy(arr, sortKeys) { const clone [...arr]; return clone.sort((a, b) { for (const conf of sortKeys) { const { field, order asc } conf; const aVal a[field]; const bVal b[field]; if (aVal bVal) return order asc ? -1 : 1; if (aVal bVal) return order asc ? 1 : -1; } return 0; }); } export { uniqueBy, sortBy };用法示例const data [ { id: 1, name: 张三, age: 30, city: 北京 }, { id: 2, name: 李四, age: 25, city: 上海 }, { id: 1, name: 张三, age: 30, city: 北京 }, { id: 3, name: 王五, age: 25, city: 北京 }, ]; // 按 id 去重 const uniqueData uniqueBy(data, id); // 先按 city 升序再按 age 升序 const sortedData sortBy(uniqueData, [ { field: city, order: asc }, { field: age, order: asc }, ]);这里有几个设计上的考量用Map而不是{}做去重映射表是因为Map的 key 支持任意类型而且迭代顺序稳定。如果用普通对象key 会被强制转成字符串而且如果你要按数字 id 去重null、undefined、__proto__这些值还容易出现意外。排序函数接收一个条件数组而不是写死单个字段好处是调用方可以灵活组合排序规则且逻辑非常自然。5.2 基础类型去重数值排序的合并写法有时我们要处理的是这样的流程接口返回一个数组里面可能有重复也可能顺序乱七八糟我们需要展示一个有序、无重复的列表。比如某个下拉框选项去重后还要按 label 排序const options [react, vue, angular, vue, svelte, react]; const uniqueSorted [...new Set(options)].sort((a, b) a.localeCompare(b)); // [angular, react, svelte, vue]对这种简单场景一行搞定完全不需要引入工具函数。5.3 去重后排序的边界情况空值处理实际业务中数据往往带有空值。排序时空值怎么处理是前端经常要跟产品确认的。比如商品列表按价格排序没有价格null的商品应该排最后还是排最前这没有统一答案不同的产品有不同的预期。常见的策略是把空值排到最后list.sort((a, b) { const aPrice a.price ?? Infinity; const bPrice b.price ?? Infinity; return aPrice - bPrice; });这里用??把null/undefined统一替换成一个足够大的数值这样没有价格的商品会沉到末尾。如果希望空值排最前就换成-Infinity。去重时也要注意空值字段。按字段去重时如果多条记录的 id 都是null它们会被视为重复最终只保留一条。这通常符合预期但也可能不符合——比如两条临时数据都没有 id但内容不同你不能把它们合并。所以去重字段最好一定得有值否则需要先做数据清洗或者把空值处理规则明确下来。6. 细节与性能从底层原理理解为什么这些写法有效6.1 Set 和 Map 底层的哈希机制Set之所以能做到近似 O(1) 的查找是因为它内部使用哈希表在 V8 中实际是哈希表 线性探测的组合实现。插入一个元素时会计算它的哈希值定位到哈希桶如果桶里已经存在相同值按SameValueZero规则判断就认为重复。这也是为什么Set对基本类型很快但对对象类型“很笨”——两个内容相同的对象它们的哈希值不同因为哈希基于对象引用所以永远不重复。Map的 key 机制类似。如果你用普通对象作为 key那去重实际上还是按引用去重没有意义。要用基本类型字段作为 key才能实现“字段值去重”。6.2 JS 内置排序到底是什么算法V8 引擎的Array.prototype.sort经过多次演变。早期版本数组长度 ≤ 10 时用插入排序大于 10 时用快速排序。快速排序不稳定而且最坏情况复杂度可能退化到 O(n²)。后来 V8 采用了 TimSort这是 Python 的list.sort也在用的算法本质上是“归并排序 插入排序”的混合体。TimSort 在“近乎有序”的数据上表现特别好复杂度接近 O(n)而随机数据上是 O(n log n)。所以咱们用内置sort()在绝大多数情况下不仅省事性能也靠谱。自己手写快排反而容易因为基准值选得不好而翻车。6.3 空间复杂度和内存占用Set去重需要额外维护一个哈希表空间复杂度 O(n)。当 n 是百万级别时可能占用不小的内存。但说实话前端场景里处理百万级纯数组的机会不多真要处理一般也是后端先做。如果前端真的遇到超大数组比如从 WebSocket 拿全量数据可能更好的方案是让后端做去重或者前端做分页 流式处理。sort()内部分配的额外内存取决于具体算法。V8 的 TimSort 在需要合并归并段时可能会申请额外数组。这个内存申请在排序结束之后会释放一般不会常驻。所以对常规业务数据无需过度焦虑。6.4 避免在排序回调里做太多事这是一个特别容易踩的性能坑。看这个写法list.sort((a, b) { const aKey a.profile.settings.theme ?? default; const bKey b.profile.settings.theme ?? default; return aKey.localeCompare(bKey); });如果list有 10 万条sort回调会被调用约 150 万次实际次数取决于算法和数据。每次调用都去深层取一次profile.settings.theme性能开销非常大。优化方法是先把排序字段提取到一个临时数组排序后再组装回对象或者用装饰模式const decorated list.map((item, index) ({ item, key: item.profile.settings.theme ?? default, index, })); decorated.sort((a, b) a.key.localeCompare(b.key) || a.index - b.index); const result decorated.map((d) d.item);这种“装饰-排序-还原”模式既避免了重复取值的开销又能保证稳定排序通过 index 做次要排序。这是一种很经典的优化思路。7. 常见问题与排查技巧实录7.1 NaN 去重失效问题[NaN, NaN].filter((item, index) arr.indexOf(item) index)得到的是[NaN, NaN]去重没生效。原因indexOf内部使用严格相等比较NaN NaN为false所以找不到。解决改用Set或includesincludes使用SameValueZero会把 NaN 视为相等。const arr [NaN, NaN]; console.log([...new Set(arr)]); // [NaN]7.2 数字和字符串自动转换导致排序错乱问题[10, 9, 2].sort()排成了[10, 9, 2]不符合预期。原因默认排序转字符串比较“10”排在“2”前面。解决确保数据类型一致并传比较函数。const arr [10, 9, 2].map(Number).sort((a, b) a - b);7.3 对象数组按数字字段排序但字段是字符串问题[{price: 10}, {price: 9}]按a.price - b.price排序结果不对实际上10 - 9会被强制转成数字JavaScript 的减号操作符会把两个操作数转为 number所以结果还是 1没问题。但如果用localeCompare或者直接比较字符串就会出现“10”排在“9”前的问题。所以最好在数据源上就保证字段类型统一。7.4 排序是原地修改的问题问题调用arr.sort()之后arr本身变了有时候逻辑上希望原数组不变。解决使用[...arr].sort(...)或arr.slice().sort(...)拷贝后再排序。7.5 localeCompare 的性能localeCompare虽然方便但性能比普通比较慢。如果只是比较 ASCII 字符串比如英文 id直接用和更快。如果是中文拼音排序才需要考虑localeCompare。7.6 多条件排序的优先级理解写多个排序条件时最容易出现的错误是“后面的条件把前面的条件覆盖了”。比如list.sort((a, b) a.age - b.age); list.sort((a, b) a.name.localeCompare(b.name));这样最后一个sort会完全忽略前面的排序因为第二次排序是在第一次排序的结果上重新排列的如果第二次排序算法稳定第一次排序只在“name 相等”的情况下才会保留相对顺序。最终效果其实等价于“先按 age 排再按 name 排”中的次要条件而不是“同时按两个字段排”。要同时按多个字段排序必须在同一个比较函数里写 if-else或者像我封装的sortBy一样循环处理。7.7 去重后数量变化不符合预期有时候你按 id 去重结果发现比预期的少。排查思路打印原始数组的 id 列表看看是不是真的存在重复。看看是不是有些 id 是字符串1有些是数字1Map 的 key 区分类型所以字符串1和数字1不会视为重复。如果你希望它们视为重复需要统一类型。看看 id 字段名是否正确。一个隐蔽的坑是接口返回的字段名带空格或下划线比如id 导致取值取不到所有项的 key 都是undefined最终只剩一条。这类问题通常在联调时出现多打 console 看看就好。8. 生产环境中的综合案例8.1 场景用户列表展示假设有一个后台管理系统需要展示用户列表。接口返回了两份数据一份来自用户服务一份来自权限服务需要按用户 id 合并去重然后按创建时间降序展示。const userServiceData [ { id: 1, name: 张三, createdAt: 2024-01-01 }, { id: 2, name: 李四, createdAt: 2024-02-01 }, ]; const authServiceData [ { id: 2, name: 李四, role: admin, createdAt: 2024-02-01 }, { id: 3, name: 王五, role: user, createdAt: 2024-03-01 }, ]; const merged [...userServiceData, ...authServiceData]; const unique uniqueBy(merged, id); const sorted sortBy(unique, [{ field: createdAt, order: desc }]);这里uniqueBy保留的是先出现的那条也就是 userServiceData 的数据。如果权限服务里更新了用户的名称或角色可能需要合并字段而不是简单丢弃。这时候可以考虑把两份数据各自处理成 Map再做字段合并。8.2 场景前端下拉框选项排序后端返回的商品分类列表可能有重复需要按分类名称拼音排序同时去重。const rawCategories [水果, 蔬菜, 水果, 肉类, 水产, 蔬菜]; const categories [...new Set(rawCategories)].sort((a, b) a.localeCompare(b, zh-Hans-CN));8.3 场景表格批量操作任务队列表格里选择多行数据做批量操作为了避免重复选择同一行需要按行 id 去重并且保持用户勾选的先后顺序。const selectedRows [ { id: 10, task: 编译 }, { id: 7, task: 测试 }, { id: 10, task: 编译 }, { id: 3, task: 发布 }, ]; const uniqueRows uniqueBy(selectedRows, id); // [{id:10, task:编译}, {id:7, task:测试}, {id:3, task:发布}]去重顺序由 Map 的迭代顺序保证所以用户先勾选的行会排在前面。9. 一些值得沉淀的经验与最终建议说到底“JS集合去重和排序”不是一个孤立的小知识点它背后涉及数据结构的选型、算法复杂度、JavaScript 语言特性、实际业务场景的边界判断这些东西组合在一起才构成了一个合格的前端开发者处理数据的基本功。我个人的经验是写任何去重排序逻辑之前先问自己三个问题第一数据是什么类型基本类型数组还是对象数组对象数组按哪个字段去重第二性能要求是什么数据量大概多大如果只有几十条就算写得烂一点也无所谓如果有几万条就得考虑用 Set/Map不能写 O(n²) 的双重循环。第三排序的语义是什么是数值大小排还是字符串字典序排还是中文拼音排空值怎么处理多字段排序的优先级是什么这些不确定的时候宁可去问产品经理也不要自己拍脑袋因为排序和去重直接影响用户看到的列表顺序错了很难察觉又很影响体验。再分享一个小技巧日常开发中把uniqueBy和sortBy这类函数沉淀到团队自己的 utils 库里写好 JSDoc 注释加上单测能省掉大量重复劳动。不要每次用到都在业务代码里重新写一遍写着写着就会写出不同行为最后维护起来很痛苦。写到这里关于“JS集合去重和排序”我能想到的坑和方案基本都过了一遍。如果你在实际开发中也有遇到过其他奇奇怪怪的去重排序问题那大概率也是某个边界条件没处理到位。回到原理层面再检查一遍数据类型、判重规则、稳定性和空值逻辑基本都能找到答案。
返回列表