JavaScript 数组去重:从 indexOf 到 Set 的七种实现与性能对比

举报
yd_252100366 发表于 2026/10/12 08:51:32 2026/10/12
【摘要】 ## 一、为什么去重是个高频问题后端接口合并多条数据、多选标签回显、URL 参数拼接……几乎每天都会遇到数组去重。ES6 之前大家手写循环,ES6 之后一句展开运算符就能搞定,但不同实现在性能、边界行为上差异很大。选错方法,轻则代码难读,重则在十万级数据下直接卡死页面。## 二、七种实现方式### 1. 双层 for 循环(最直观)```jsfunction unique(arr) {  c...

## 一、为什么去重是个高频问题

后端接口合并多条数据、多选标签回显、URL 参数拼接……几乎每天都会遇到数组去重。ES6 之前大家手写循环,ES6 之后一句展开运算符就能搞定,但不同实现在性能、边界行为上差异很大。选错方法,轻则代码难读,重则在十万级数据下直接卡死页面。

## 二、七种实现方式

### 1. 双层 for 循环(最直观)

```js
function unique(arr) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < result.length; j++) {
      if (arr[i] === result[j]) break;
    }
    if (j === result.length) result.push(arr[i]);
  }
  return result;
}
```

注意内层循环的 j 要用 let 声明在 for 里,循环结束后 j 的作用域正好覆盖判断。时间复杂度 O(n²),只适合几十条数据。

### 2. filter + indexOf

```js
function unique(arr) {
  return arr.filter((item, index) => arr.indexOf(item) === index);
}
```

一行搞定,逻辑是"只保留第一次出现的元素"。但 indexOf 内部也是循环,整体仍是 O(n²)。

### 3. 排序后相邻比较

```js
function unique(arr) {
  const sorted = [...arr].sort();
  return sorted.filter((item, i) => item !== sorted[i - 1]);
}
```

排序把相同元素挤到一起,一次遍历即可去重,复杂度降到 O(n log n)。注意 sort 默认按字符串比较,[1, 10, 2] 会排成 [1, 10, 2],混用类型时先传比较函数。

### 4. Map 记录已见元素

```js
function unique(arr) {
  const seen = new Map();
  return arr.filter((item) => {
    if (seen.has(item)) return false;
    seen.set(item, true);
    return true;
  });
}
```

Map 查找 O(1),整体 O(n),且严格保留原顺序,是需要兼容老环境时的首选。

### 5. Set 展开(日常最推荐)

```js
const unique = (arr) => [...new Set(arr)];
```

Set 内部基于哈希,天然去重且保持插入顺序,O(n) 一步到位。日常开发首选,代码短到不需要注释。

### 6. reduce 累加

```js
function unique(arr) {
  return arr.reduce((acc, item) => (acc.includes(item) ? acc : [...acc, item]), []);
}
```

函数式写法,可读性好,但 includes 每次扫描结果数组,且每次展开都创建新数组,数据量大时性能和内存都不好看。

### 7. 对象键唯一性(ES5 时代的技巧)

```js
function unique(arr) {
  const seen = {};
  return arr.filter(
    (item) =>
      seen.hasOwnProperty(typeof item + item)
        ? false
        : (seen[typeof item + item] = true)
  );
}
```

利用对象属性名唯一的特性,加 typeof 前缀是为了区分数字 1 和字符串 '1'。ES6 普及后已无使用场景,理解思想即可。

## 三、性能对比(10 万条随机整数)

| 方案 | 复杂度 | 耗时参考 | 保留原顺序 |
| --- | --- | --- | --- |
| 双层循环 | O(n²) | ~1200ms | 是 |
| filter + indexOf | O(n²) | ~900ms | 是 |
| 排序相邻比较 | O(n log n) | ~60ms | 否 |
| Map 记录 | O(n) | ~15ms | 是 |
| Set 展开 | O(n) | ~10ms | 是 |
| reduce | O(n²) | ~800ms | 是 |

数据量越大差距越悬殊:一万条以内体感差异不大,超过五万条,O(n²) 方案就会明显阻塞主线程。

## 四、三个容易踩的边界

1. **NaN 去不掉**:indexOf 用的是严格相等,NaN !== NaN,所以 filter + indexOf 去不掉 NaN;Set 用的是 SameValueZero 比较,[...new Set([NaN, NaN])] 的结果是 [NaN],这是 Set 的隐藏优势。
2. **对象按引用去重**:Set 判断的是引用地址,两个内容相同的对象字面量不会被去重。要按内容去重,先映射成唯一键(如 JSON.stringify)再套 Set。
3. **稀疏数组**:[1, , 2] 中的空位在 filter 里会被跳过,去重结果可能和预期不一致,先用 [...arr] 展开补齐再处理。

## 五、总结

日常开发直接 [...new Set(arr)];需要兼容 IE 或老环境时用 Map 版本;数据量小于百条时哪种写法都不重要,可读性优先。去重本质上是用空间换时间——单次 O(n) 的方案都依赖哈希结构,理解这一点就能自己判断该选哪种。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。