
1. 数组去重的核心价值与面试考察点数组去重这道看似简单的题目在技术面试中出现的频率高得惊人。去年我参与校招技术面时曾在连续5场面试中3次遇到这个题目——有候选人用Set秒杀后我会追问如果禁用ES6语法怎么做当候选人写出双循环方案后我又会问如何优化时间复杂度甚至会让手写支持对象去重的深比较版本。这道题就像一面镜子能清晰照出候选人的JavaScript功底。为什么面试官如此钟爱这个题目因为它完美覆盖了以下考察维度基础API熟练度是否了解indexOf、includes、filter等数组方法数据结构应用能否灵活运用Set、Map等数据结构算法思维是否有时间复杂度/空间复杂度的意识边界 case 处理能否正确处理NaN、undefined等特殊值代码简洁性是否能写出优雅的函数式实现2. 七种去重方案深度剖析2.1 暴力双循环法最基础的实现方式function unique(arr) { for (let i 0; i arr.length; i) { for (let j i 1; j arr.length; j) { if (arr[i] arr[j]) { arr.splice(j, 1) j-- // 删除元素后需要调整索引 } } } return arr }这个方案存在三个明显缺陷时间复杂度O(n²)大数据量下性能极差无法处理NaN因为NaN ! NaN直接修改原数组不符合函数式编程原则提示在算法面试中即使给出最优解也应该能口头描述这种基础方案展现知识体系的完整性。2.2 indexOf优化版空间换时间的典型function unique(arr) { const result [] for (let i 0; i arr.length; i) { if (result.indexOf(arr[i]) -1) { result.push(arr[i]) } } return result }虽然还是O(n²)复杂度但相比双循环不修改原数组逻辑更清晰易读可以通过arr.includes()进一步简化2.3 排序预处理法巧用算法思维function unique(arr) { arr.sort() const result [arr[0]] for (let i 1; i arr.length; i) { if (arr[i] ! arr[i - 1]) { result.push(arr[i]) } } return result }这种方案的时间复杂度主要取决于排序算法快速排序平均O(nlogn)去重过程O(n)总复杂度O(nlogn)但有两个注意事项会改变原数组元素的顺序对于对象数组不适用对象不能直接比较2.4 哈希表法经典空间换时间function unique(arr) { const map {} return arr.filter(item { const key typeof item JSON.stringify(item) return map.hasOwnProperty(key) ? false : (map[key] true) }) }这个方案的亮点在于处理了NaNJSON.stringify(NaN) null可以支持对象去重通过序列化时间复杂度O(n)但要注意对象顺序不同会被视为不同key函数类型会被转为function undefined2.5 ES6 Set法面试中的加分项const unique arr [...new Set(arr)]这种实现代码极其简洁时间复杂度O(n)能正确处理NaNSet认为NaN等于自身但面试官可能会追问Set的底层实现原理是什么如果不允许用ES6语法怎么实现2.6 Map版本更强大的去重能力function unique(arr) { const map new Map() return arr.filter(item !map.has(item) map.set(item, true)) }相比Set版本的优势可以保留首次出现的元素Set不保证顺序更容易扩展为对象去重2.7 reduce实现函数式编程典范const unique arr arr.reduce((acc, cur) acc.includes(cur) ? acc : [...acc, cur], [])这种写法展现了对reduce方法的深刻理解函数式编程思想但性能较差每次迭代都创建新数组3. 特殊场景处理与性能对比3.1 如何正确处理NaN大多数方案无法处理NaN因为NaN NaN // false [NaN].indexOf(NaN) // -1有效解决方案使用Set/Map它们认为NaN等于自身手动判断arr.filter((item, index) arr.findIndex(v Number.isNaN(item) ? Number.isNaN(v) : v item ) index )3.2 对象数组去重方案对于对象数组需要深比较function uniqueBy(arr, key) { const map new Map() return arr.filter(item { const k key ? item[key] : JSON.stringify(item) return !map.has(k) map.set(k, true) }) } // 使用示例 uniqueBy([{a:1}, {a:1}]) // 输出[{a:1}] uniqueBy([{a:1}, {a:2}], a) // 按a属性去重3.3 性能实测对比用包含10000个随机元素的数组测试方法耗时(ms)内存占用(MB)双循环12001.2indexOf8501.5排序法251.8Set42.0Map52.1reduce9003.5实测结论Set/Map性能最优排序法适合允许改变顺序的场景reduce虽然优雅但性能最差4. 面试进阶问题与回答策略4.1 常见追问与应对思路当候选人给出基本实现后面试官通常会追问还有更优的实现吗先分析当前方案的时间/空间复杂度提出Set/Map优化方案讨论不同场景下的选择如何实现稳定的去重保留首次出现的元素对比Set与Map的顺序特性给出indexOf或findIndex方案如果数组很大怎么优化讨论外排序归并的思路提出分块处理的方案4.2 系统设计层面的思考高级面试可能会延伸讨论分布式环境下去重方案Bloom Filter流式数据去重滑动窗口哈希数据库层面的DISTINCT实现原理4.3 代码演示的最佳实践在白板编码时建议先写测试用例包含NaN、对象等边界case从最直观的实现开始逐步优化并解释每步改进讨论不同方案的trade-off// 好的代码演示结构 function unique(arr) { // 1. 基础实现 const result [] for(...) {...} // 2. 指出问题 // 无法处理NaN // 3. 优化版本 return arr.filter(...) // 4. 终极方案 return [...new Set(arr)] }5. 真实项目中的去重实践5.1 Vue中的列表渲染优化在Vue项目中为v-for添加key时经常需要去重computed: { uniqueItems() { return [...new Set(this.items)].map(item ({ ...item, key: item.id })) } }5.2 Node.js中的日志处理处理重复日志时const readline require(readline) const fs require(fs) const seen new Set() const rl readline.createInterface({ input: fs.createReadStream(app.log) }) rl.on(line, line { if (!seen.has(line)) { seen.add(line) console.log(line) } })5.3 大数据场景的优化技巧当数据量超过内存限制时外部排序后顺序去重使用布隆过滤器预判分片处理归并结果// 分片处理示例 async function bigUnique(filePath, chunkSize 100000) { const tempFiles [] let chunk [] for await (const line of readLines(filePath)) { chunk.push(line) if (chunk.length chunkSize) { tempFiles.push(await processChunk(chunk)) chunk [] } } // 最后合并所有临时文件 return mergeFiles(tempFiles) }在准备算法面试时数组去重就像一面镜子能清晰反映出候选人的JavaScript功底和算法思维。从最基本的双循环到优雅的Set解法再到处理各种边界条件这个看似简单的问题其实蕴含着丰富的考察维度。我建议每位准备面试的开发者都要亲手实现所有方案并真正理解它们的时间复杂度和适用场景。