Skip to content

题目

请实现数组去重,包括基础数组和对象数组的去重,并说明多种实现方式及其优缺点。

📝 标准答案

核心要点

数组去重根据数据类型分为两大类:

1. 基础数组去重

  • ES6 Set:最推荐的现代方案
  • filter + indexOf:传统方案,兼容性好
  • Map/includes:可以正确处理NaN
  • 双层循环:最基础的实现

2. 对象数组去重

  • 按引用去重:Set可以处理(但通常不是我们想要的)
  • 按属性去重:根据某个属性判断重复(最常用)
  • 按值去重:对象内容完全相同才算重复(深度比较)

方案对比表

数据类型方案时间复杂度NaN处理推荐度
基础数组SetO(n)⭐⭐⭐⭐⭐
基础数组filter+indexOfO(n²)⭐⭐⭐
基础数组includesO(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)算法

💡 面试回答技巧

推荐回答顺序

  1. 先问清楚需求:是基础数组还是对象数组?去重依据是什么?
  2. 基础数组用Set:写出最简洁的方案,说明优势
  3. 解释Set原理:哈希表、SameValueZero算法
  4. 补充传统方案:filter+indexOf等,展示知识广度
  5. 对象数组用Map:按属性去重,说明实现原理
  6. 提到特殊情况: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: 主要区别:

特性SetArray
元素唯一性✅ 自动去重❌ 可重复
查找性能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

🔗 相关知识点

📚 参考资料

基于 MIT 许可发布 | 隐私政策