JavaScript 数组去重:从 indexOf 到 Set 的七种实现与性能对比
## 一、为什么去重是个高频问题
后端接口合并多条数据、多选标签回显、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) 的方案都依赖哈希结构,理解这一点就能自己判断该选哪种。
- 点赞
- 收藏
- 关注作者
评论(0)