重复项从何而来,为什么这是个问题?
\n想象一下:您从 API 收集数据、解析 CSV 文件或处理用户输入。在 90% 的情况下,您会遇到数组中同一个元素出现多次的情况。这不仅仅是一个美观问题——重复项可能会扭曲统计数据、破坏列表渲染或导致算法陷入无限循环。
\nStack Overflow 上的问题 "Remove duplicate values from a JavaScript array" 获得数千次浏览并非偶然。这是一项基础但至关重要的操作,每个开发人员都应该掌握。今天,我们将深入底层,解析不同方法的运作方式——从最朴素的方法到最高效的方法。
\n\n方法 #1:Set——最优雅且最快的方法
\n如果您问一位现代 JavaScript 开发人员:“如何删除重复项?”,他们首先会提到 Set。这是正确的。但它是如何工作的呢?
const numbers = [1, 2, 2, 3, 4, 4, 5];\nconst uniqueNumbers = [...new Set(numbers)];\nconsole.log(uniqueNumbers); // [1, 2, 3, 4, 5]\n\nSet 底层发生了什么?
\nSet 对象存储任何类型的唯一值。当您将数组传递给构造函数 new Set(numbers) 时,JavaScript 引擎(V8、SpiderMonkey 等)会执行以下步骤:
- \n
- 遍历数组:引擎遍历原始数组的每个元素。 \n
- 唯一性检查:对每个元素调用内部哈希函数。如果该元素已存在于“桶”(bucket)中,则忽略它。如果不存在,则添加它。 \n
- 组装结果:展开运算符
[...set]将可迭代的 Set 对象展开回数组。 \n
一个重要细节:Set 使用 SameValueZero 比较算法。这意味着 NaN 被认为等于 NaN(与 === 不同),而 +0 和 -0 被视为不同的值。在实践中这很少产生影响,但值得了解。
Set 的性能
\n操作的复杂度为 O(n)。每个元素只检查一次。这是所有方法中最好的指标。对于包含 10,000 个元素的数组,Set 与其他方法之间的差异可能达到几十毫秒——这对于高负载应用程序至关重要。
\n\n方法 #2:filter + indexOf——经典组合
\n在 ES6(2015)出现之前,开发人员使用 filter 和 indexOf 的组合。这种方法至今仍出现在遗留代码中:
const numbers = [1, 2, 2, 3, 4, 4, 5];\nconst uniqueNumbers = numbers.filter((value, index, self) => {\n return self.indexOf(value) === index;\n});\nconsole.log(uniqueNumbers); // [1, 2, 3, 4, 5]\n\n它是如何工作的?
\n- \n
filter遍历数组的每个元素。 \n - 对每个元素调用
indexOf(value),它返回该值第一次出现的索引。 \n - 如果当前元素的索引等于第一次找到的索引——这意味着它是第一次出现,元素被保留。如果不相等——则是重复项,被丢弃。 \n
陷阱
\n主要问题是性能。indexOf 内部使用线性搜索(每次调用 O(n))。因此,对于包含 N 个元素的数组,复杂度变为 O(n²)。对于 1000 个元素,这还可以接受,但对于 100,000 个元素,性能下降会变得明显。
另一个缺点:indexOf 无法正确处理 NaN。如果数组中包含 NaN,indexOf(NaN) 将始终返回 -1,并且该元素会被重复添加。要检查 NaN,需要使用