1. 项目概述为什么“去重”是前端开发者的基本功在JavaScript的日常开发里处理数据是家常便饭而数组和对象又是其中最核心的数据结构。我敢说几乎每个前端开发者都遇到过这样的场景从后端拿到一个用户列表里面可能有重复的用户ID或者处理一组商品数据需要合并不同来源但可能重复的商品信息。这时候“去重”就成了一个绕不开的操作。简单数组去重比如[1, 2, 2, 3]变成[1, 2, 3]方法很多Set、filter加indexOf信手拈来。但问题一旦升级到对象数组事情就变得棘手了。[{id: 1}, {id: 1}]这两个对象看起来一样但在JavaScript引擎眼里它们是两个独立的内存引用直接比较{} {}结果是false。这就意味着那些对简单数组行之有效的方法在对象数组面前几乎全部失效。“JS对象数组去重”这个标题背后直指的就是这个高频且具体的痛点。它不是一个炫技的算法题而是一个实实在在的工程问题。处理不好轻则导致前端展示重复用户体验下降重则可能在数据统计、状态同步时引发逻辑错误。因此掌握一套可靠、高效且适应不同场景的对象数组去重方案是区分一个合格前端和熟练前端的重要标志之一。接下来我就结合自己多年的踩坑经验把对象数组去重的门道给你彻底讲透。2. 核心思路拆解从“相等”的定义出发对象数组去重的核心在于如何定义两个对象“相等”。对于计算机来说没有模糊的概念我们必须给出精确、可执行的判断规则。根据业务场景的不同这个“相等”的定义通常分为几个层次选择的方案也截然不同。2.1 基于唯一标识符的去重这是最常见、也最实用的场景。对象数组中每个对象都有一个或多个属性可以唯一标识它自己比如用户的id、商品的sku、文章的postId。我们的目标就是保留这些唯一标识符首次出现的对象。为什么这是首选方案因为在真实的业务数据中对象往往是复杂且动态的。除了核心ID其他属性如name,price,status可能会因为数据来源不同、更新时间不同而有细微差异。如果我们追求所有属性完全一致反而可能丢失有效的数据版本。基于唯一标识符去重逻辑清晰符合大多数业务语义例如同一个用户不应该在列表中出现两次。实现思路我们需要一个临时存储通常用Map或普通对象来记录已经出现过的“键”。遍历数组为每个对象生成一个“键”通常是标识符属性的值检查这个键是否已存在。如果不存在则记录该键并将当前对象放入结果数组如果已存在则跳过。2.2 基于对象全等比较的去重这种场景相对较少但确实存在。比如你需要确保数组中的每个对象引用都是唯一的或者你的数据对象结构简单且稳定要求所有属性值必须完全一致才视为重复。为什么使用场景有限因为JavaScript中对象是引用类型。即使两个对象的内容一模一样它们也是不同的引用。因此直接比较obj1 obj2只有在它们指向内存中同一地址时才为真。要实现“内容全等”比较就需要深度遍历对象的每一个属性进行递归或序列化比较性能开销较大且对于包含函数、循环引用的对象处理起来很麻烦。实现思路通常采用序列化的方式将对象转换为字符串如JSON.stringify然后用字符串去重的方法。但这种方法有局限性函数、undefined、特定对象类型会被忽略或转换且性能不是最优。更严谨的做法是实现一个深度比较函数但复杂度高一般只在特殊需求下使用。2.3 基于自定义比较函数的去重这是最灵活的方式。当“相等”的逻辑不能用简单的属性名或深度比较概括时就需要自定义。例如去重规则是“姓名和城市相同即视为同一人”或者“价格相差在5元以内视为相同商品”当然这严格来说不是去重是聚类但逻辑类似。为什么需要灵活性业务逻辑是千变万化的。框架和库提供的是通用能力而自定义比较函数是将业务规则注入到工具方法中的桥梁。它把判断两个对象是否“重复”的权力完全交给了开发者。实现思路实现一个通用的去重函数它接受一个数组和一个比较函数作为参数。这个比较函数接收两个对象返回一个布尔值表示它们是否相等。在内部仍然需要通过遍历和临时存储来记录已经出现过的“等价类”但比较逻辑由外部函数决定。3. 方案实现与深度解析理论讲完了我们直接上代码看看每种思路具体怎么实现并深入分析其中的细节和陷阱。3.1 方案一使用 Map 与唯一键推荐这是目前性能最佳、语义最清晰的方案适用于绝大多数基于标识符去重的场景。/** * 根据对象中指定的唯一键进行去重 * param {Array} arr - 待去重的对象数组 * param {String|Function} key - 唯一键的属性名或一个生成唯一键的函数 * returns {Array} 去重后的新数组 */ function uniqueByKey(arr, key) { // 参数校验 if (!Array.isArray(arr)) { throw new TypeError(Expected an array as the first argument); } if (arr.length 0) return []; const map new Map(); const result []; for (const item of arr) { // 处理key为函数的情况允许动态生成唯一标识 const identifier typeof key function ? key(item) : item[key]; // 关键检查标识符是否为有效值避免undefined或null作为键导致的问题 if (identifier null) { // 宽松相等检查 null 和 undefined // 处理策略可以选择跳过、抛出错误或允许其通过。这里我们选择跳过并给出警告生产环境可记录日志 console.warn(Item with invalid key (${identifier}) encountered and skipped:, item); continue; } // 如果Map中还没有这个标识符则存入并加入结果数组 if (!map.has(identifier)) { map.set(identifier, true); // 值存true即可我们只关心键是否存在 result.push(item); } // 如果已存在则跳过。这里可以根据需要保留第一次或最后一次出现的项。 // 当前逻辑保留第一次出现的项。 } return result; } // 使用示例 const users [ { id: 1, name: Alice }, { id: 2, name: Bob }, { id: 1, name: Alice Again }, // 重复的id { id: 3, name: Charlie }, { id: 2, name: Bob the Second }, // 重复的id { name: NoID } // 缺少id属性的对象 ]; console.log(uniqueByKey(users, id)); // 输出: [{ id: 1, name: Alice }, { id: 2, name: Bob }, { id: 3, name: Charlie }] // 注意NoID对象被跳过并警告 // 使用函数生成复杂键 const orders [ { userId: 1, productId: A, date: 2023-10-01 }, { userId: 1, productId: B, date: 2023-10-01 }, { userId: 1, productId: A, date: 2023-10-02 }, { userId: 2, productId: A, date: 2023-10-01 }, ]; // 去重逻辑同一用户在同一日期下的同一产品只保留第一单 const uniqueOrders uniqueByKey(orders, (order) ${order.userId}-${order.productId}-${order.date}); console.log(uniqueOrders);深度解析与注意事项为什么用Map而不用普通对象{}键的类型Map的键可以是任何类型包括对象、函数而普通对象的键只能是字符串或 Symbol。虽然我们的标识符通常是字符串或数字但使用Map更具通用性和严谨性避免了数字键被自动转换为字符串等隐式转换问题。性能在频繁的增删查操作中Map的性能通常优于普通对象尤其是在键的数量较多时。顺序Map会记住键的原始插入顺序这在某些需要保持去重后顺序的场景下是个优点虽然我们这里用数组本身来保证顺序。对key参数的处理支持字符串和函数两种形式极大地增强了灵活性。函数形式让你可以处理复合键、计算键等复杂场景。空值处理这是非常关键的一点如果对象的标识符属性是undefined或nullMap可以存储它们Map可以存undefined和null作为键但这通常意味着数据有问题。上面的实现选择跳过并警告防止无效数据污染结果集。在实际项目中你需要和业务方确认对此类数据的处理策略。保留首次还是末次上述代码保留首次出现的项这是最常见的需求。如果你想保留最后一次出现的项只需将result.push(item)的逻辑改为更新对应位置但这会更复杂。一个简单的技巧是反向遍历数组然后反转结果但会改变相对顺序。更清晰的做法是在Map里存储对象本身最后用Array.from(map.values())但这会丢失首次出现之后、末次出现之前其他对象的顺序。3.2 方案二使用 JSON.stringify 与 Set慎用这个方案常被新手想到因为它代码非常简短。function uniqueByJSON(arr) { if (!Array.isArray(arr)) return []; const stringSet new Set(); const result []; for (const obj of arr) { const str JSON.stringify(obj); if (!stringSet.has(str)) { stringSet.add(str); result.push(obj); } } return result; }深度解析与严重缺陷警告此方法不推荐用于生产环境仅适用于非常特定的、可控的简单场景。序列化陷阱JSON.stringify有众所周知的局限性undefined、函数、Symbol 类型的属性值会被完全忽略不会出现在字符串中。{a: undefined, b: 1}和{b: 1}会被认为是相同的。如果对象有循环引用直接调用会报错。NaN和Infinity会被转换成null。Date对象会被转换成字符串。属性的顺序可能会影响序列化结果虽然ECMA规范未定义对象属性的枚举顺序但大多数现代引擎会按创建顺序不过依赖这个并不安全。性能问题对于大对象或大数组序列化整个对象是昂贵的操作尤其是当对象结构复杂时。什么情况下可以用仅当你100%确定数组中的对象是简单的、平面的没有嵌套对象/数组、不包含上述特殊值、并且属性顺序稳定时可以作为一种“快速原型”手段。即便如此我也建议用更明确的方案一。3.3 方案三使用 reduce 与 find/findIndex理解思路但不推荐这是一种更“函数式”的写法但在性能上存在隐患。// 使用 findIndex 进行深度比较假设有 deepEqual 函数 function uniqueByDeepCompare(arr) { return arr.reduce((acc, current) { // 在累积数组acc中查找是否已存在“深度相等”的对象 const isDuplicate acc.some(item deepEqual(item, current)); if (!isDuplicate) { acc.push(current); } return acc; }, []); } // 使用 findIndex 基于某个键 function uniqueByKeyWithReduce(arr, key) { return arr.reduce((acc, current) { const isDuplicate acc.findIndex(item item[key] current[key]) -1; if (!isDuplicate) { acc.push(current); } return acc; }, []); }深度解析与性能瓶颈算法复杂度这是这种方法最大的问题。对于数组中的每一个元素n个都要在结果数组最坏情况下也是n个中遍历查找findIndex或some是 O(n) 操作。这导致了 O(n²) 的时间复杂度。当数组长度超过几百时性能下降会非常明显。deepEqual的代价如果使用深度比较每次比较的代价 O(k)k为对象大小会叠加在 O(n²) 上使得性能雪上加霜。可读性虽然reduce很强大但这段代码的逻辑不如方案一中的for...of循环配合Map那样直观易懂尤其是对不熟悉函数式编程的同事。结论不推荐在需要处理可能较大数组的场景下使用此方法。方案一Map的时间复杂度是 O(n)空间复杂度也是 O(n)性能优势巨大。3.4 方案四终极灵活方案——自定义比较函数将比较逻辑抽象出来提供一个通用的去重工具函数。/** * 通用对象数组去重函数 * param {Array} arr - 待去重的对象数组 * param {Function} comparator - 比较函数接收两个对象返回true表示相等 * returns {Array} 去重后的新数组 */ function uniqueByComparator(arr, comparator) { if (!Array.isArray(arr)) return []; if (typeof comparator ! function) { throw new TypeError(Comparator must be a function); } const result []; // 这里我们仍然需要一个机制来记录“已见过”的对象。 // 但由于比较规则自定义我们无法简单地用一个键来记录。 // 一种方法是对于result中的每个新元素都遍历result中已存在的元素进行比较。 // 但这又回到了O(n²)的复杂度。 // 更优的方法是要求comparator能生成一个可哈希的“签名”或者接受一个额外的keyGetter函数。 // 下面提供一个更实用的变体它结合了key生成器和比较器。 return result; // 基础框架实现见下方变体 } /** * 增强版结合键生成器和比较器优先使用键进行高效去重键冲突时使用比较器 * param {Array} arr * param {Function} keyGetter - 生成用于快速查找的键的函数 * param {Function} [comparator] - 可选当键冲突时用于精细比较的函数 * returns {Array} */ function uniqueAdvanced(arr, keyGetter, comparator) { const map new Map(); const result []; for (const item of arr) { const key keyGetter(item); // 如果键无效处理策略同方案一 if (key null) { console.warn(Invalid key generated, item skipped:, item); continue; } if (!map.has(key)) { // 如果这个键第一次出现直接存入 map.set(key, item); result.push(item); } else if (comparator) { // 如果键已存在并且提供了比较器则用比较器判断当前对象和已存储的对象是否“重复” const existingItem map.get(key); if (!comparator(existingItem, item)) { // 如果比较器认为不重复注意这里逻辑是“不重复才添加”根据comparator语义调整 // 但通常相同的key我们已经认为是同一类这里comparator用于处理“key相同但实际不同”的边缘情况。 // 更常见的需求是key相同且comparator也认为相同才去重。否则我们需要一个新的、不冲突的key // 这揭示了设计上的复杂性。通常keyGetter应该能生成绝对唯一的标识。 // 因此comparator在这里可能不是必须的或者用于二次确认。 // 一个更简单的通用设计是只使用comparator但用Map存储序列化后的比较结果这又回到了性能问题。 // 结论对于极度复杂的去重逻辑可能需要专门定制算法而非通用函数。 } } // 如果键已存在且没有提供comparator或comparator认为重复则跳过保留首次出现的 } return result; }深度解析与设计权衡这个方案展示了设计通用工具的复杂性。纯comparator的方案会导致性能低下O(n²)。而keyGetter方案本质上就是我们的方案一。keyGettercomparator的混合模式试图在效率和灵活性间取得平衡但逻辑变得复杂且comparator的调用场景键冲突时可能很少。实操建议99%的场景使用方案一uniqueByKey就足够了。确保你的数据有一个可靠的主键或复合键。对于那1%需要复杂判等逻辑的场景认真评估是否真的需要通用的去重函数。也许针对那个特定场景写一个特殊的去重逻辑更简单、更高效。如果一定要写通用函数可以考虑让comparator函数同时返回一个用于快速查找的“哈希码”不要求严格唯一但能大大减少需要深度比较的候选对但这实现起来就更复杂了。4. 性能对比与实战选型光说不练假把式我们写个简单的测试来对比一下方案一Map、方案三ReducefindIndex和方案二JSON的性能差异。我们构造一个包含10000个对象的数组其中约有30%的重复项。// 生成测试数据 function generateTestData(size, duplicateRate) { const data []; for (let i 0; i size; i) { data.push({ id: i, value: Value${i}, nested: { prop: Math.random() } }); } // 添加一些重复项 const duplicateCount Math.floor(size * duplicateRate); for (let i 0; i duplicateCount; i) { const randomIndex Math.floor(Math.random() * size); data.push({ ...data[randomIndex] }); // 浅拷贝创建内容相同但引用不同的对象 } return data.sort(() Math.random() - 0.5); // 打乱顺序 } const testData generateTestData(10000, 0.3); console.log(测试数据量${testData.length}); // 方案一Map console.time(uniqueByKey-Map); const result1 uniqueByKey(testData, id); console.timeEnd(uniqueByKey-Map); console.log(结果长度${result1.length}); // 方案三Reduce findIndex (基于键) console.time(uniqueByKey-Reduce); const result3 testData.reduce((acc, current) { const isDuplicate acc.findIndex(item item.id current.id) -1; if (!isDuplicate) acc.push(current); return acc; }, []); console.timeEnd(uniqueByKey-Reduce); console.log(结果长度${result3.length}); // 方案二JSON (仅作对比数据符合其要求) // 注意我们的测试数据包含nested对象和Math.randomJSON序列化后由于nested.prop值不同重复项可能无法被正确识别。 // 为了公平对比我们使用一个更简单的数据。 const simpleData generateTestData(10000, 0.3).map(({id, value}) ({id, value})); // 只保留id和value console.time(uniqueByJSON); const result2 uniqueByJSON(simpleData); console.timeEnd(uniqueByJSON); console.log(结果长度${result2.length});在我的环境中运行一次结果可能类似测试数据量13000 uniqueByKey-Map: 2.5ms 结果长度10000 uniqueByKey-Reduce: 150.0ms 结果长度10000 uniqueByJSON: 15.0ms (在简单数据上) 结果长度10000结果分析Map方案~2.5ms速度最快时间复杂度 O(n)与数据量成线性关系即使数据量增大性能衰减也最平缓。Reduce findIndex方案~150ms慢了两个数量级这是因为其 O(n²) 的复杂度。当数据量翻倍时耗时可能增加近4倍。JSON方案~15ms在简单数据上表现尚可但如前所述它有严格的适用条件且序列化本身也有开销。实战选型指南默认选择Map方案无论是性能、代码清晰度还是安全性都是最佳选择。用它处理基于唯一标识符的去重。永远避免Reduce findIndex全量查找方案除非你能绝对保证数组长度永远很小比如小于50否则不要使用。谨慎使用JSON方案仅用于临时性的、数据格式极其简单的场景并且要充分了解其缺陷。不要将其作为默认方案。复杂逻辑定制化如果去重逻辑异常复杂无法用单一键表示优先考虑在数据源头进行处理或者编写专门的、非通用的函数来解决。牺牲一定的通用性来换取可读性和性能是值得的。5. 特殊场景与边界情况处理在实际项目中数据从来都不是完美的。下面是一些常见的“坑”以及如何处理它们。5.1 处理可能为空的标识符我们在方案一的代码中已经初步处理了。这里再强调一下策略跳过并记录如上所示这是比较安全的做法避免无效数据影响主要结果。适用于标识符缺失为异常情况的场景。保留并视为特殊值如果null或undefined本身就是有意义的标识虽然不常见你可以允许它们作为Map的键。但要注意Map可以区分null、undefined和不存在而普通对象{}做不到。抛出错误如果标识符是必填的缺失属于数据错误应该尽早抛出异常让调用者处理。5.2 需要保留最后一次出现的对象业务需求有时是“保留最新的那条记录”。这时方案一稍作修改即可。function uniqueByKeyKeepLast(arr, key) { const map new Map(); // 第一遍遍历用Map记录每个键最后一次出现的对象 for (const item of arr) { const identifier typeof key function ? key(item) : item[key]; if (identifier ! null) { map.set(identifier, item); // 始终用最新的对象覆盖 } } // 第二遍遍历按原始顺序或标识符顺序输出但每个键只取最后一次的值 // 注意如果要严格保持原数组中“最后一次出现”的相对顺序需要更复杂的逻辑。 // 简单的方法是直接返回Map的值但顺序是Map的插入顺序即键第一次出现的顺序。 // 如果顺序不重要 // return Array.from(map.values()); // 如果需要按照键的最后一次出现在原数组中的顺序 const result []; const seenKey new Set(); // 倒序遍历原数组这样先遇到的是最后一次出现 for (let i arr.length - 1; i 0; i--) { const item arr[i]; const identifier typeof key function ? key(item) : item[key]; if (identifier ! null !seenKey.has(identifier)) { seenKey.add(identifier); // 因为我们是倒序插入所以需要插入到结果数组的头部或者最后反转数组 result.unshift(item); // unshift在数组头部插入但大数据量下性能差 } } // 或者用正序遍历但用Map存储索引最后排序逻辑更复杂。 // 一个平衡性能和逻辑清晰的做法是用Map存储对象再用一个数组记录顺序。 const orderMap new Map(); const orderArr []; for (const item of arr) { const identifier typeof key function ? key(item) : item[key]; if (identifier ! null) { orderMap.set(identifier, item); // 记录顺序如果重复更新索引不我们需要最后一次的顺序。 // 更简单遍历完成后再逆序处理。 } } // ... 代码会变得冗长。根据具体性能要求和数据规模选择实现。 // 对于大多数情况如果顺序不是严格必须Array.from(map.values()) 是可接受的。 return Array.from(map.values()); }可以看到保留末次的逻辑比保留首次要复杂尤其是对顺序有要求时。在需求评审时尽量明确“保留首次”这更符合直觉和大多数场景。5.3 超大数组的性能与内存考虑当数组长度达到十万甚至百万级别时即使是 O(n) 的算法也需要考虑优化。使用Map而非{}如前所述Map在大量键值对时性能更好。避免在循环中创建临时对象比如key是函数且返回新对象这会导致大量小对象被创建和垃圾回收。流式处理如果数据来自文件或网络流可以考虑边读取边去重而不是全部加载到内存中再处理。这需要数据源支持迭代。使用更高效的数据结构在极端性能要求下如果键是数字或特定范围的字符串可以考虑使用Array或TypedArray作为哈希表但这牺牲了通用性。5.4 嵌套对象与循环引用如果你的对象非常深且基于嵌套属性去重keyGetter函数需要能安全地访问深层次属性。可以使用lodash的_.get或自己写一个安全访问函数。function getSafe(obj, path, defaultValue) { return path.split(.).reduce((acc, key) (acc acc[key] ! undefined) ? acc[key] : defaultValue, obj); } const data [{ user: { profile: { id: 123 } } }, { user: { profile: { id: 456 } } }]; const key (item) getSafe(item, user.profile.id, null); const uniqueData uniqueByKey(data, key);对于循环引用JSON.stringify会直接报错。如果去重逻辑涉及序列化必须确保数据中没有循环引用或者使用可以处理循环引用的序列化库如flatted。6. 在现代JS项目中的集成与实践掌握了核心方法我们来看看如何将它优雅地集成到你的项目中。6.1 封装为工具函数或类方法在你的项目工具库例如src/utils/array.js中导出稳定的去重函数。// utils/array.js export const uniqueBy (arr, key) { // ... 实现方案一包含健壮的错误处理 }; export const uniqueByKeepLast (arr, key) { // ... 实现保留末次的版本 }; // 或者提供一个配置更全的函数 export const unique (arr, { key, comparator, keep first } {}) { // 根据参数选择不同的内部实现 };6.2 与 Lodash 或 Ramda 等工具库对比像lodash这样的库提供了_.uniqBy和_.uniqWith函数。_.uniqBy(array, [iteratee_.identity])类似于我们的uniqueByKeyiteratee可以是属性名字符串或函数。_.uniqWith(array, [comparator])使用自定义比较函数但注意它内部可能也是 O(n²) 的复杂度用于小型数组或特殊比较。使用建议如果你的项目已经引入了lodash并且其体积不是问题直接使用_.uniqBy是很好的选择它经过充分测试处理了各种边界情况。如果你追求极致的包体积或者想避免引入大型工具库那么自己实现一个轻量级的uniqueByKey是更优解。我们的实现通常只有十几行代码功能完全够用。6.3 在Vue/React状态管理中的应用在前端框架中去重操作经常发生在处理状态时。Vue (Pinia) 示例// stores/userStore.js import { defineStore } from pinia; import { uniqueBy } from /utils/array; export const useUserStore defineStore(user, { state: () ({ userList: [], }), actions: { // 从API合并用户列表并去重 mergeUsers(newUsers) { const merged [...this.userList, ...newUsers]; this.userList uniqueBy(merged, id); }, // 或者作为一个getter }, getters: { // 获取去重后的用户列表计算属性 uniqueUsers: (state) uniqueBy(state.userList, id), }, });React (Redux Toolkit) 示例// features/users/usersSlice.js import { createSlice } from reduxjs/toolkit; import { uniqueBy } from ../../utils/array; const usersSlice createSlice({ name: users, initialState: { list: [] }, reducers: { usersReceived(state, action) { // 假设action.payload是新获取的用户数组 const merged [...state.list, ...action.payload]; state.list uniqueBy(merged, id); }, }, }); // 在组件中 import { useSelector } from react-redux; const uniqueUserList useSelector(state uniqueBy(state.users.list, id));关键点在状态管理中去重应该作为一个纯函数被调用确保相同的输入永远得到相同的输出不产生副作用。这符合Redux和Vuex/Pinia的设计原则。6.4 与异步数据流结合RxJS在处理流数据时去重也是一个常见操作。import { from, of } from rxjs; import { mergeMap, toArray, reduce } from rxjs/operators; // 假设有一个发出用户对象数组的Observable const userObservable from([ [{id: 1, name: A}, {id: 2, name: B}], [{id: 2, name: B}, {id: 3, name: C}], // 包含重复的id:2 [{id: 1, name: A}, {id: 4, name: D}], // 包含重复的id:1 ]); // 我们需要合并所有发出的数组并去重 userObservable.pipe( // 将每个发出的数组合并成一个数组 reduce((acc, currentArray) acc.concat(currentArray), []), // 对最终合并的数组进行去重 mergeMap(combinedArray of(uniqueBy(combinedArray, id))) ).subscribe(uniqueUsers { console.log(去重后的用户列表:, uniqueUsers); // 输出: [{id:1,name:A}, {id:2,name:B}, {id:3,name:C}, {id:4,name:D}] });在RxJS中还有distinct、distinctUntilChanged等操作符用于流中单个值的去重但针对对象数组的合并去重通常还是需要在最终阶段使用我们实现的工具函数。7. 总结与个人心得对象数组去重这个看似简单的问题深入下去却涉及数据结构选择、算法复杂度、API设计、边界处理以及与现代开发流的结合。经过上面一番梳理我的核心建议可以总结为三点第一明确“相等”语义是前提。在动手写代码之前一定要和产品经理或后端同事确认清楚到底什么叫“重复”是基于ID还是基于几个字段的组合抑或是所有字段完全一致这个定义直接决定了实现方案。第二Map 唯一键是王道。对于99%的业务场景基于Map和对象唯一标识符的方案是最佳选择。它性能好O(n)代码清晰易于理解和维护。自己封装一个uniqueByKey函数处理好null/undefined键的边界情况就能覆盖绝大部分需求。第三警惕性能陷阱和语法糖诱惑。JSON.stringify虽然写起来短但坑太多不要用它处理重要数据。array.reduce配合array.find看起来很“函数式”但 O(n²) 的复杂度在数据量稍大时就会成为性能瓶颈。在追求代码简洁的同时一定要心里有性能这根弦。最后分享一个我自己的习惯在工具函数中永远加上参数类型校验和简单的错误提示。就像我们在uniqueByKey里做的那样检查输入是否为数组。这行代码可能一辈子都不会触发但一旦触发比如有人不小心传了个null进来它能为你节省大量的调试时间。好的工具函数不仅是能干活还要能“友好地”告诉调用者哪里用错了。