题目
请实现数组去重,包括基础数组和对象数组的去重,并说明多种实现方式及其优缺点。
📝 标准答案
核心要点
数组去重根据数据类型分为两大类:
1. 基础数组去重
- ES6 Set:最推荐的现代方案
- filter + indexOf:传统方案,兼容性好
- Map/includes:可以正确处理NaN
- 双层循环:最基础的实现
2. 对象数组去重
- 按引用去重:Set可以处理(但通常不是我们想要的)
- 按属性去重:根据某个属性判断重复(最常用)
- 按值去重:对象内容完全相同才算重复(深度比较)
方案对比表
| 数据类型 | 方案 | 时间复杂度 | NaN处理 | 推荐度 |
|---|---|---|---|---|
| 基础数组 | Set | O(n) | ✅ | ⭐⭐⭐⭐⭐ |
| 基础数组 | filter+indexOf | O(n²) | ❌ | ⭐⭐⭐ |
| 基础数组 | includes | O(n²) | ✅ | ⭐⭐⭐ |
| 对象数组 | Map按属性 | O(n) | - | ⭐⭐⭐⭐⭐ |
| 对象数组 | reduce按属性 | O(n²) | - | ⭐⭐⭐ |
| 对象数组 | JSON深度比较 | O(n²) | - | ⭐⭐ |
🧠 深度理解
基础数组去重原理
1. Set的实现原理
Set内部使用哈希表存储值,使用SameValueZero算法比较:
javascript
// Set的特点
const set = new Set([1, 2, 2, 3]);
console.log(set); // Set(3) {1, 2, 3}
// 正确处理NaN
const set2 = new Set([NaN, NaN]);
console.log(set2.size); // 1,NaN被认为是相同的
// 对象按引用比较
const obj = {a: 1};
const set3 = new Set([obj, obj, {a: 1}]);
console.log(set3.size); // 2,相同引用的obj被去重2. indexOf的局限性
javascript
// indexOf无法正确处理NaN
[NaN, NaN].indexOf(NaN); // -1,因为 NaN !== NaN
// 导致去重失败
[1, NaN, 2, NaN].filter((item, index, arr) => {
return arr.indexOf(item) === index;
});
// 结果: [1, NaN, 2, NaN],NaN没有被去重 ❌
// includes可以正确处理
[NaN, NaN].includes(NaN); // true ✅对象数组去重原理
1. 为什么Set不能去重对象?
javascript
const obj1 = {id: 1};
const obj2 = {id: 1};
obj1 === obj2; // false,不同的引用
// Set按引用比较
const arr = [obj1, obj2, obj1];
[...new Set(arr)]; // [obj1, obj2],只去掉了重复的obj1引用2. Map按属性去重的优势
javascript
function uniqueBy(arr, key) {
const map = new Map();
return arr.filter(item => {
const value = item[key];
if (map.has(value)) {
return false; // 已存在,过滤掉
}
map.set(value, true);
return true; // 首次出现,保留
});
}
// Map的查找是O(1),比indexOf的O(n)快很多常见误区
误区1:认为所有去重方法都能正确处理NaN
- 正解:只有Set、Map、includes、Object.is能正确处理
误区2:认为Set可以去重对象数组(按内容)
- 正解:Set只能按引用去重,需要自己实现按属性去重
误区3:使用JSON.stringify进行深度比较
- 正解:JSON.stringify有局限性(属性顺序、特殊值等)
误区4:对大数据量使用O(n²)算法
- 正解:应该使用Set、Map等O(n)算法
💡 面试回答技巧
推荐回答顺序
- 先问清楚需求:是基础数组还是对象数组?去重依据是什么?
- 基础数组用Set:写出最简洁的方案,说明优势
- 解释Set原理:哈希表、SameValueZero算法
- 补充传统方案:filter+indexOf等,展示知识广度
- 对象数组用Map:按属性去重,说明实现原理
- 提到特殊情况:NaN处理、深度比较、多属性去重等
🎯 一句话回答(快速版)
基础数组用ES6的Set:[...new Set(arr)],对象数组用Map按属性去重,都是O(n)时间复杂度,能正确处理NaN。
📣 口语化回答(推荐)
数组去重分两种情况:
基础数组去重最推荐ES6的Set,一行代码搞定:[...new Set(arr)]。优势很明显:代码简洁、性能好(O(n)时间复杂度)、还能正确处理NaN。传统的filter+indexOf有两个问题:性能差(O(n²))和无法识别NaN。
对象数组去重就复杂一些,因为Set是按引用比较的,内容相同但引用不同的对象不会去重。所以需要明确去重依据,最常见的是按某个属性去重,比如按id。推荐用Map实现,把属性值作为key,时间复杂度还是O(n)。
如果需要深度比较(对象内容完全相同才算重复),可以用JSON.stringify,但要注意局限性。实际项目中,简单场景自己写Map去重,复杂场景用lodash的uniqBy。
重点强调
- 强调Set方案的优势:代码简洁、性能好(O(n))、正确处理NaN
- 说明对象数组去重必须明确去重依据
- 提到实际项目中通常使用lodash的uniqBy
- 说明时间复杂度的重要性(O(n) vs O(n²))
可能的追问
Q1: 为什么indexOf无法识别NaN?
A: 因为NaN的特殊性:NaN !== NaN,而indexOf内部使用严格相等(===)比较。
javascript
NaN === NaN; // false
NaN == NaN; // false
// 解决方案
Object.is(NaN, NaN); // true
[NaN].includes(NaN); // true,使用SameValueZero算法Q2: Set和Array的区别是什么?
A: 主要区别:
| 特性 | Set | Array |
|---|---|---|
| 元素唯一性 | ✅ 自动去重 | ❌ 可重复 |
| 查找性能 | O(1) | O(n) |
| 访问方式 | 迭代器 | 索引 |
| 适用场景 | 去重、存在性检查 | 有序列表、随机访问 |
Q3: 如何实现对象数组的深度去重?
A: 有几种方法,但都有局限性:
javascript
// 方法1: JSON.stringify(简单但有局限)
function deepUnique(arr) {
const seen = new Set();
return arr.filter(item => {
const str = JSON.stringify(item);
if (seen.has(str)) return false;
seen.add(str);
return true;
});
}
// 局限性:属性顺序、undefined、function等
// 方法2: 使用lodash(推荐)
import { isEqual, uniqWith } from 'lodash';
const result = uniqWith(arr, isEqual);💻 代码示例
基础数组去重
方法一:ES6 Set(推荐)
javascript
function uniqueBySet(arr) {
return [...new Set(arr)];
}
// 使用
const arr = [1, 2, 2, 3, NaN, NaN, 4];
console.log(uniqueBySet(arr)); // [1, 2, 3, NaN, 4] ✅方法二:filter + indexOf
javascript
function uniqueByIndexOf(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
// 注意:无法正确处理 NaN
const arr = [1, NaN, 2, NaN, 3];
console.log(uniqueByIndexOf(arr)); // [1, NaN, 2, NaN, 3] ❌方法三:includes(处理 NaN)
javascript
function uniqueByIncludes(arr) {
const result = [];
arr.forEach(item => {
if (!result.includes(item)) {
result.push(item);
}
});
return result;
}
// 可以正确处理 NaN
const arr = [1, NaN, 2, NaN, 3];
console.log(uniqueByIncludes(arr)); // [1, NaN, 2, 3] ✅对象数组去重
方法一:Map按属性去重(推荐)
javascript
function uniqueBy(arr, key) {
const map = new Map();
return arr.filter(item => {
const value = item[key];
if (map.has(value)) return false;
map.set(value, true);
return true;
});
}
// 简化版
const uniqueByShort = (arr, key) => {
const map = new Map();
return arr.filter(item => !map.has(item[key]) && map.set(item[key], true));
};方法二:多属性组合去重
javascript
function uniqueByKeys(arr, keys) {
const map = new Map();
return arr.filter(item => {
const key = keys.map(k => item[k]).join('|');
if (map.has(key)) return false;
map.set(key, true);
return true;
});
}
// 使用示例
const data = [
{id: 1, type: 'A'},
{id: 1, type: 'B'},
{id: 1, type: 'A'}, // 重复
];
uniqueByKeys(data, ['id', 'type']); // 去掉最后一个方法三:深度去重(按值比较)
javascript
function deepUnique(arr) {
const seen = new Set();
return arr.filter(item => {
const str = JSON.stringify(item);
if (seen.has(str)) return false;
seen.add(str);
return true;
});
}
// 注意:JSON.stringify有局限性完整代码示例
查看完整代码(包含所有方法和测试用例):array-dedup-complete.js