JavaScript 中的数组去重方法详解
字数 2023 2025-12-13 03:51:33
JavaScript 中的数组去重方法详解
我们来详细讲解 JavaScript 中数组去重的各种方法,包括它们的原理、优缺点、使用场景和性能差异。
一、问题描述
数组去重是指从包含重复元素的数组中,创建一个新数组,其中每个元素只出现一次。这是 JavaScript 开发中最常见的操作之一,有多种实现方式,每种方式在性能、可读性和适用场景上都有所不同。
二、常用去重方法详解
方法1:使用 Set(ES6+)
这是目前最简洁、最高效的去重方法。
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = [...new Set(array)];
// 或者
const uniqueArray2 = Array.from(new Set(array));
实现步骤:
new Set(array)创建一个 Set 对象- Set 是 ES6 新增的数据结构,它类似于数组,但成员的值都是唯一的
- Set 会自动过滤掉重复的值
- 使用扩展运算符
...或Array.from()将 Set 转换回数组
优点:
- 代码简洁,一行搞定
- 性能优秀,时间复杂度 O(n)
- 能正确处理所有原始类型(包括 undefined 和 null)
限制:
- 对于引用类型(对象、数组),是基于引用比较,不是值比较
- 不兼容旧版浏览器(IE 不支持 Set)
示例对比:
// 对原始类型有效
const arr1 = [1, 2, 2, 'hello', 'hello', true, true];
console.log([...new Set(arr1)]); // [1, 2, 'hello', true]
// 对引用类型无效(基于引用比较)
const obj = {name: 'John'};
const arr2 = [obj, obj, {name: 'John'}];
console.log([...new Set(arr2)]); // [{name: 'John'}, {name: 'John'}] 两个对象
方法2:使用 filter 和 indexOf
这是传统的 ES5 方法,兼容性好。
function unique(array) {
return array.filter((item, index) => array.indexOf(item) === index);
}
实现步骤:
array.indexOf(item)返回元素首次出现的索引- 只保留索引与首次出现位置相同的元素
- 通过
filter创建新数组
工作原理:
- 第一次遇到元素 2(索引 1):
indexOf(2)返回 1,条件成立,保留 - 第二次遇到元素 2(索引 2):
indexOf(2)返回 1,条件不成立,过滤
优点:
- ES5 语法,兼容性好
- 代码相对简洁
- 返回新数组,不改变原数组
缺点:
- 时间复杂度 O(n²),大数组性能差
- 无法区分
NaN(indexOf(NaN)总是返回 -1)
示例:
const array = [1, 2, 2, 3, 4, 4, 5];
const result = array.filter((item, index) => array.indexOf(item) === index);
console.log(result); // [1, 2, 3, 4, 5]
// NaN 处理问题
const arrWithNaN = [1, NaN, NaN, 2];
console.log(arrWithNaN.filter((item, idx) => arrWithNaN.indexOf(item) === idx));
// [1, NaN, NaN, 2] 应该只有一个 NaN
方法3:使用 reduce
通过 reduce 累积唯一元素。
function unique(array) {
return array.reduce((acc, current) => {
if (!acc.includes(current)) {
acc.push(current);
}
return acc;
}, []);
}
实现步骤:
- 初始化为空数组
[] - 遍历原数组每个元素
- 如果累积数组中不存在当前元素,就添加
- 返回累积数组
优点:
- 函数式编程风格
- 逻辑清晰
- 可扩展性强(可添加复杂逻辑)
缺点:
- 性能相对较差(每次都要
includes检查) - 同样有
NaN识别问题
优化版本(使用对象缓存):
function unique(array) {
const seen = {};
return array.reduce((acc, current) => {
const key = typeof current + JSON.stringify(current);
if (!seen[key]) {
seen[key] = true;
acc.push(current);
}
return acc;
}, []);
}
方法4:双重循环
最基础的去重算法。
function unique(array) {
const result = [];
for (let i = 0; i < array.length; i++) {
let isDuplicate = false;
for (let j = 0; j < result.length; j++) {
if (array[i] === result[j]) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
result.push(array[i]);
}
}
return result;
}
实现步骤:
- 外层遍历原数组
- 内层检查结果数组是否已包含当前元素
- 不包含则添加到结果数组
优点:
- 逻辑最直观
- 不依赖任何内置方法
- 可完全控制比较逻辑
缺点:
- 时间复杂度 O(n²),性能最差
- 代码冗长
方法5:使用 Map(ES6+)
Map 也可以用于去重,比对象更灵活。
function unique(array) {
const map = new Map();
return array.filter(item => {
if (!map.has(item)) {
map.set(item, true);
return true;
}
return false;
});
}
实现步骤:
- 创建 Map 记录已出现的元素
- 遍历数组,检查 Map 中是否存在
- 不存在则记录并保留,存在则过滤
优点:
- 性能优秀(Map 的 has 和 set 是 O(1))
- 可正确处理任何类型(包括对象,基于引用)
- 保持元素顺序
示例:
const array = [1, 2, 2, 3, 4, 4, 5];
const map = new Map();
const result = array.filter(item => {
if (!map.has(item)) {
map.set(item, true);
return true;
}
return false;
});
console.log(result); // [1, 2, 3, 4, 5]
三、特殊情况的处理
1. NaN 的处理
NaN 是 JavaScript 中唯一不等于自身的值,需要特殊处理。
解决方案:
function unique(array) {
const set = new Set();
const result = [];
let hasNaN = false;
for (const item of array) {
if (item !== item) { // 检查是否为 NaN
if (!hasNaN) {
hasNaN = true;
result.push(NaN);
}
} else if (!set.has(item)) {
set.add(item);
result.push(item);
}
}
return result;
}
2. 对象去重(基于值比较)
默认方法基于引用比较,基于值的比较需要序列化。
基于 JSON.stringify 的方法:
function uniqueByValue(array) {
const seen = new Set();
return array.filter(obj => {
const key = JSON.stringify(obj);
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
局限性:
- 无法处理循环引用
- 属性顺序不同会被视为不同对象
- 函数、undefined 等会被 JSON.stringify 忽略
更好的方案(使用深度比较):
function deepEqual(a, b) {
if (a === b) return true;
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
return false;
}
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
3. 按特定属性去重
针对对象数组,按某个属性去重。
function uniqueByProperty(array, property) {
const seen = new Set();
return array.filter(obj => {
const value = obj[property];
if (seen.has(value)) {
return false;
}
seen.add(value);
return true;
});
}
// 使用示例
const users = [
{id: 1, name: 'Alice'},
{id: 2, name: 'Bob'},
{id: 1, name: 'Alice'}, // 重复
{id: 3, name: 'Charlie'}
];
const uniqueUsers = uniqueByProperty(users, 'id');
// [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}, {id: 3, name: 'Charlie'}]
四、性能比较
通过一个性能测试来比较不同方法:
function testPerformance(arraySize) {
const testArray = [];
for (let i = 0; i < arraySize; i++) {
testArray.push(Math.floor(Math.random() * 100));
}
const methods = [
{name: 'Set', fn: arr => [...new Set(arr)]},
{name: 'Filter', fn: arr => arr.filter((v, i) => arr.indexOf(v) === i)},
{name: 'Reduce', fn: arr => arr.reduce((acc, cur) =>
acc.includes(cur) ? acc : [...acc, cur], [])},
{name: 'Map', fn: arr => {
const map = new Map();
return arr.filter(item => {
if (!map.has(item)) {
map.set(item, true);
return true;
}
return false;
});
}},
{name: 'Double Loop', fn: arr => {
const result = [];
for (let i = 0; i < arr.length; i++) {
let found = false;
for (let j = 0; j < result.length; j++) {
if (arr[i] === result[j]) {
found = true;
break;
}
}
if (!found) result.push(arr[i]);
}
return result;
}}
];
const results = {};
methods.forEach(({name, fn}) => {
const start = performance.now();
fn(testArray);
const end = performance.now();
results[name] = (end - start).toFixed(3) + 'ms';
});
return results;
}
console.log('1000个元素:', testPerformance(1000));
console.log('10000个元素:', testPerformance(10000));
典型性能排序(从快到慢):
- Set 方法 - O(n),最快
- Map 方法 - O(n),接近 Set
- Filter + indexOf - O(n²),中等规模尚可
- Reduce - O(n²),比 filter 稍慢
- 双重循环 - O(n²),最慢
五、实际应用建议
-
现代项目(支持 ES6+):
- 优先使用
[...new Set(array)] - 简洁、高效、可读性好
- 优先使用
-
兼容旧浏览器:
- 使用 filter + indexOf
- 或自己实现兼容版本:
function unique(arr) { if (typeof Set !== 'undefined') { return Array.from(new Set(arr)); } return arr.filter((v, i) => arr.indexOf(v) === i); } -
对象数组去重:
- 使用 Map 记录特定属性
- 或使用 lodash 的
_.uniqBy
-
大数据量场景:
- 必须使用 Set 或 Map
- 避免 O(n²) 算法
-
需要保持顺序:
- Set 和 Map 都保持插入顺序
- filter + indexOf 保持首次出现顺序
六、总结
数组去重是 JavaScript 基础但重要的操作。选择方法时需要考虑:
- 数据类型(原始类型 vs 引用类型)
- 数据规模
- 浏览器兼容性
- 是否需要保持顺序
- 性能要求
对于现代开发,[...new Set(array)] 是最佳选择,兼顾了简洁性、性能和可读性。对于特殊需求,可以根据具体情况选择或实现相应的方法。