JavaScript 数组去重方法汇总
JavaScript 数组去重方法汇总
一、Introduction(引言)
数组去重是 JavaScript 开发中使用频率极高的基础操作,几乎每个前端工程师在日常编码中都会遇到这一需求。无论是从后端获取的数据需要过滤重复项、用户表单提交的数组需要清洗、还是日常处理日志或统计数据时需要消除重复记录,掌握一套完整、高效的数组去重方法都是必备技能。本文将系统梳理 JavaScript 中去除数组重复元素的各类实现方案,涵盖从最基础的 for 循环到最新的 Set 特性,从一维数组到对象数组的深度去重,从时间复杂度分析到实际业务场景的选择建议,帮助读者在不同需求下快速找到最适合的解决方案。
二、基础语法与核心方法
2.1 Set 去重(最简洁方案)
ES6 引入的 Set 数据结构天然保证元素唯一性,是目前最简洁、运行效率最高的去重方式,适用于基本类型数组。
// 基本用法
const arr = [1, 2, 2, 3, 3, 3, 'apple', 'apple', true, true, NaN, NaN];
const unique = [...new Set(arr)];
console.log(unique);
// 输出: [1, 2, 3, 'apple', true, NaN]
// 注意:Set 认为 NaN 与 NaN 相同,所以只保留一个 NaN
// Array.from + Set(另一种写法)
const unique2 = Array.from(new Set(arr));
console.log(unique2);
// 输出: [1, 2, 3, 'apple', true, NaN]
2.2 indexOf / includes 去重
利用数组的 indexOf() 或 includes() 方法,在遍历时检查新数组中是否已存在该元素。
// indexOf 去重
function uniqueByIndexOf(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (result.indexOf(arr[i]) === -1) {
result.push(arr[i]);
}
}
return result;
}
// includes 去重(更语义化,ES7+)
function uniqueByIncludes(arr) {
const result = [];
for (const item of arr) {
if (!result.includes(item)) {
result.push(item);
}
}
return result;
}
const arr = [1, 2, 2, 3, 'hello', 'hello', null, null];
console.log(uniqueByIncludes(arr));
// 输出: [1, 2, 3, 'hello', null]
2.3 filter + indexOf 去重
利用 filter 配合 indexOf,返回元素首次出现的索引等于当前遍历索引的位置,实现优雅的去重。
function uniqueByFilter(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'grape'];
console.log(uniqueByFilter(fruits));
// 输出: ['apple', 'banana', 'orange', 'grape']
2.4 reduce + includes 去重
函数式编程风格,将去重逻辑浓缩为一行链式调用。
function uniqueByReduce(arr) {
return arr.reduce((acc, cur) => {
return acc.includes(cur) ? acc : [...acc, cur];
}, []);
}
const nums = [1, 1, 2, 2, 3, 3, 4, 5, 5];
console.log(uniqueByReduce(nums));
// 输出: [1, 2, 3, 4, 5]
2.5 对象键值对去重
利用对象的键不可重复特性,可高效去重,但需要注意类型转换问题(如 1 和 '1' 会被视为同一键)。
function uniqueByObject(arr) {
const obj = {};
const result = [];
for (const item of arr) {
const key = typeof item + '_' + JSON.stringify(item);
if (!obj[key]) {
obj[key] = true;
result.push(item);
}
}
return result;
}
const mixed = [1, '1', true, 'true', {a:1}, {a:1}];
console.log(uniqueByObject(mixed));
// 输出: [1, '1', true, 'true', {a:1}, {a:1}]
// 注意:对象由于引用不同,即使内容相同也被视为不同元素
2.6 双层 for 循环+splice 去重
传统方法,通过 splice 在原数组上原地删除重复项,适合面试手写实现。
function uniqueBySplice(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1);
j--; // splice 后数组缩短,需调整索引
}
}
}
return arr;
}
const arr = [1, 2, 2, 3, 3, 3, 4];
console.log(uniqueBySplice([...arr])); // 复制一份再处理
// 输出: [1, 2, 3, 4]
三、代码示例
3.1 基本类型数组去重
// 模拟从后端获取的用户ID列表(含重复)
const userIds = [101, 102, 103, 102, 101, 105, 103, 107, 106, 105, 108];
// 方法1:Set(推荐)
const uniqueIds = [...new Set(userIds)];
console.log('去重后:', uniqueIds);
// 输出: [101, 102, 103, 105, 107, 106, 108]
// 方法2:filter
const uniqueIds2 = userIds.filter((id, idx) => userIds.indexOf(id) === idx);
console.log('filter去重:', uniqueIds2);
// 输出: [101, 102, 103, 105, 107, 106, 108]
3.2 对象数组按指定字段去重
// 订单列表,含重复用户ID的记录
const orders = [
{ id: 1, userId: 'u001', product: 'iPhone', amount: 8999 },
{ id: 2, userId: 'u002', product: 'MacBook', amount: 12999 },
{ id: 3, userId: 'u001', product: 'AirPods', amount: 1299 },
{ id: 4, userId: 'u003', product: 'iPad', amount: 4999 },
{ id: 5, userId: 'u002', product: 'Apple Watch', amount: 2999 },
{ id: 6, userId: 'u001', product: 'Pencil', amount: 799 },
];
// 按 userId 去重,保留每个用户的第一条记录
function uniqueByField(arr, field) {
const seen = new Set();
return arr.filter(item => {
if (seen.has(item[field])) {
return false;
}
seen.add(item[field]);
return true;
});
}
const uniqueOrders = uniqueByField(orders, 'userId');
console.log('按userId去重:');
uniqueOrders.forEach(o => console.log( id=${o.id}, userId=${o.userId}, product=${o.product}));
// 输出:
// id=1, userId=u001, product=iPhone
// id=2, userId=u002, product=MacBook
// id=4, userId=u003, product=iPad
3.3 多字段组合去重
// 同时按 userId 和 product 组合去重
function uniqueByFields(arr, ...fields) {
const seen = new Set();
return arr.filter(item => {
const key = fields.map(f => item[f]).join('|');
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
const uniqueByCombo = uniqueByFields(orders, 'userId', 'product');
console.log('按userId+product去重后数量:', uniqueByCombo.length);
// 如果原始没有完全重复的组合,则不删除任何记录
3.4 模糊去重(忽略大小写/空格)
// 字符串数组去重,忽略大小写和首尾空格
const names = [' Alice ', 'Bob', 'ALICE', 'bob ', 'Charlie', 'alice', 'David'];
function uniqueIgnoreCase(arr) {
const seen = new Set();
return arr.filter(item => {
const normalized = item.trim().toLowerCase();
if (seen.has(normalized)) return false;
seen.add(normalized);
return true;
});
}
console.log(uniqueIgnoreCase(names));
// 输出: [' Alice ', 'Bob', 'Charlie', 'David']
// 保留了第一条的原始格式(包含空格)
3.5 复杂对象深度去重(JSON.stringify)
// 通过 JSON.stringify 将对象转为唯一字符串实现深度去重
function uniqueDeep(arr) {
const seen = new Set();
return arr.filter(item => {
const key = JSON.stringify(item);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
const products = [
{ name: 'iPhone', color: 'black' },
{ name: 'iPhone', color: 'black' },
{ name: 'iPhone', color: 'white' },
{ name: { a: 1 }, b: 2 },
{ name: { a: 1 }, b: 2 },
];
console.log('深度去重结果:', uniqueDeep(products).length);
// 输出: 3({name:'iPhone',color:'black'} 被去重一条,{name:'iPhone',color:'white'} 保留,{a:1,b:2} 被去重一条)
四、运行效果
4.1 Set 方案运行结果
const arr = [1, 2, 2, 'hello', 'hello', NaN, null, undefined, undefined];
console.log([...new Set(arr)]);
// [1, 2, 'hello', NaN, null, undefined]
// NaN 被正确识别为相同值,只保留一个
// null 和 undefined 各保留一个
4.2 性能对比测试
// 生成 10000 个随机数(1~1000),统计各方法耗时
const testArr = Array.from({length: 10000}, () => Math.floor(Math.random() * 1000));
console.time('Set');
[...new Set(testArr)];
console.timeEnd('Set');
// Set: ~0.5ms
console.time('filter+indexOf');
testArr.filter((item, idx) => testArr.indexOf(item) === idx);
console.timeEnd('filter+indexOf');
// filter+indexOf: ~800ms(O(n²) 复杂度,数据量大时性能急剧下降)
console.time('reduce+includes');
testArr.reduce((acc, cur) => acc.includes(cur) ? acc : [...acc, cur], []);
console.timeEnd('reduce+includes');
// reduce+includes: ~1200ms(更慢)
console.time('object');
const obj = {};
testArr.filter(v => obj[typeof v + v] ? false : (obj[typeof v + v] = true));
console.timeEnd('object');
// object: ~2ms(效率接近 Set,但实现更复杂)
结论:基本类型去重首选 Set,性能最优且代码简洁。对象数组去重使用 Map 或 Set 配合字段值作为 key,性能远优于 filter+indexOf 的 O(n²) 方案。
4.3 业务场景运行示例
// 场景:从API获取标签列表,去重后渲染前端标签云
const rawTags = ['前端', 'JavaScript', 'Node.js', '前端', 'JavaScript', 'Vue', 'React', 'Node.js', 'CSS', 'JavaScript'];
const tagCloud = [...new Set(rawTags)];
console.log('标签云:', tagCloud);
// ['前端', 'JavaScript', 'Node.js', 'Vue', 'React', 'CSS']
五、常见问题
Q1:为什么 new Set([1, '1']) 会同时保留 1 和 '1'?
Set 使用 SameValueZero 算法比较元素,1(数字)和 '1'(字符串)类型不同,因此被视为不同值都会保留。如果需要忽略类型进行去重,应手动在添加到 Set 前统一类型(如转为字符串)。
// 忽略类型去重
const arr = [1, '1', 2, '2', 3];
const result = [...new Set(arr.map(v => String(v)))].map(v => Number(v));
console.log(result); // [1, 2, 3]
Q2:indexOf 无法正确识别 NaN,如何处理?
Array.prototype.indexOf() 使用严格相等(===),而 NaN === NaN 返回 false,导致 NaN 无法被正确去重识别。解决方案是使用 Set 或自定义比较函数。
const arr = [NaN, 1, NaN, 2, NaN];
// filter+indexOf 方案:NaN 不会被识别为重复
console.log(arr.filter((v, i) => arr.indexOf(v) === i));
// [NaN, 1, 2] —— NaN 出现了多次(indexOf 每次都返回 -1)
// Set 方案:NaN 被正确去重
console.log([...new Set(arr)]);
// [NaN, 1, 2] —— 正确
Q3:对象数组去重使用 JSON.stringify 有什么隐患?
JSON.stringify 对属性的顺序敏感,{a:1, b:2} 和 {b:2, a:1} 序列化结果不同,会被视为不同对象。此外,函数(undefined)会在序列化过程中被忽略或报错。处理复杂对象建议使用自定义哈希函数或 Map 配合字段比较。
// 隐患示例
const obj1 = {a: 1, b: 2};
const obj2 = {b: 2, a: 1};
console.log(JSON.stringify(obj1) === JSON.stringify(obj2)); // true(意外相同)
// 但对于有序属性不同的对象
const obj3 = {a: 1, b: {x: 1}};
const obj4 = {a: 1, b: {y: 2}};
console.log(JSON.stringify(obj3) === JSON.stringify(obj4)); // false(正常)
Q4:如何保留重复项中最后一条记录而不是第一条?
将 filter 中比较逻辑改为 indexOf 改为 lastIndexOf,即可保留最后一条。
function uniqueKeepLast(arr) {
return arr.filter((item, index) => {
return arr.lastIndexOf(item) === index;
});
}
const arr = [1, 2, 2, 3, 3, 3];
console.log(uniqueKeepLast(arr)); // [1, 2, 3](和保留第一条结果相同,仅顺序不同场景有区别)
Q5:Map 能否用于去重?与 Set 有何区别?
Map 和 Set 都能实现去重,区别在于 Map 适用于「键值对」场景(可以保留额外信息),Set 适用于单一值去重。对于对象数组按字段去重,用 Map 以字段值为 key 最为高效。
// Map 去重(保留最后一条,并附带额外信息)
function uniqueWithMap(arr, key) {
const map = new Map();
arr.forEach(item => map.set(item[key], item)); // 后面的覆盖前面的
return [...map.values()];
}
// Set 去重(最简洁值去重)
function uniqueWithSet(arr) {
return [...new Set(arr)];
}
六、延伸阅读
- MDN Web Docs - Set:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
- MDN Web Docs - Array.prototype.filter():https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
- MDN Web Docs - Array.prototype.reduce():https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
- TC39 - SameValueZero Algorithm:https://tc39.es/ecma262/#sec-samevaluezero
- 《JavaScript高级程序设计(第4版)》 第 5 章「引用类型」- Array 类型
- lodash 库 uniq 系列函数源码:https://github.com/lodash/lodash(生产级去重方案参考)
- LeetCode - 环形链表 / 数组去重题目:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/