从数组中删除重复项:JavaScript 底层解析

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

重复项从何而来,为什么这是个问题?

\n

想象一下:您从 API 收集数据、解析 CSV 文件或处理用户输入。在 90% 的情况下,您会遇到数组中同一个元素出现多次的情况。这不仅仅是一个美观问题——重复项可能会扭曲统计数据、破坏列表渲染或导致算法陷入无限循环。

\n

Stack Overflow 上的问题 "Remove duplicate values from a JavaScript array" 获得数千次浏览并非偶然。这是一项基础但至关重要的操作,每个开发人员都应该掌握。今天,我们将深入底层,解析不同方法的运作方式——从最朴素的方法到最高效的方法。

\n\n

方法 #1:Set——最优雅且最快的方法

\n

如果您问一位现代 JavaScript 开发人员:“如何删除重复项?”,他们首先会提到 Set。这是正确的。但它是如何工作的呢?

\n\n
const numbers = [1, 2, 2, 3, 4, 4, 5];\nconst uniqueNumbers = [...new Set(numbers)];\nconsole.log(uniqueNumbers); // [1, 2, 3, 4, 5]
\n\n

Set 底层发生了什么?

\n

Set 对象存储任何类型的唯一值。当您将数组传递给构造函数 new Set(numbers) 时,JavaScript 引擎(V8、SpiderMonkey 等)会执行以下步骤:

\n
    \n
  • 遍历数组:引擎遍历原始数组的每个元素。
  • \n
  • 唯一性检查:对每个元素调用内部哈希函数。如果该元素已存在于“桶”(bucket)中,则忽略它。如果不存在,则添加它。
  • \n
  • 组装结果:展开运算符 [...set] 将可迭代的 Set 对象展开回数组。
  • \n
\n

一个重要细节:Set 使用 SameValueZero 比较算法。这意味着 NaN 被认为等于 NaN(与 === 不同),而 +0 和 -0 被视为不同的值。在实践中这很少产生影响,但值得了解。

\n\n

Set 的性能

\n

操作的复杂度为 O(n)。每个元素只检查一次。这是所有方法中最好的指标。对于包含 10,000 个元素的数组,Set 与其他方法之间的差异可能达到几十毫秒——这对于高负载应用程序至关重要。

\n\n

方法 #2:filter + indexOf——经典组合

\n

在 ES6(2015)出现之前,开发人员使用 filter 和 indexOf 的组合。这种方法至今仍出现在遗留代码中:

\n\n
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\n

陷阱

\n

主要问题是性能。indexOf 内部使用线性搜索(每次调用 O(n))。因此,对于包含 N 个元素的数组,复杂度变为 O(n²)。对于 1000 个元素,这还可以接受,但对于 100,000 个元素,性能下降会变得明显。

\n

另一个缺点:indexOf 无法正确处理 NaN。如果数组中包含 NaN,indexOf(NaN) 将始终返回 -1,并且该元素会被重复添加。要检查 NaN,需要使用

博客

书籍推荐