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));

实现步骤:

  1. new Set(array) 创建一个 Set 对象
    • Set 是 ES6 新增的数据结构,它类似于数组,但成员的值都是唯一的
    • Set 会自动过滤掉重复的值
  2. 使用扩展运算符 ...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);
}

实现步骤:

  1. array.indexOf(item) 返回元素首次出现的索引
  2. 只保留索引与首次出现位置相同的元素
  3. 通过 filter 创建新数组

工作原理:

  • 第一次遇到元素 2(索引 1):indexOf(2) 返回 1,条件成立,保留
  • 第二次遇到元素 2(索引 2):indexOf(2) 返回 1,条件不成立,过滤

优点:

  • ES5 语法,兼容性好
  • 代码相对简洁
  • 返回新数组,不改变原数组

缺点:

  • 时间复杂度 O(n²),大数组性能差
  • 无法区分 NaNindexOf(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;
  }, []);
}

实现步骤:

  1. 初始化为空数组 []
  2. 遍历原数组每个元素
  3. 如果累积数组中不存在当前元素,就添加
  4. 返回累积数组

优点:

  • 函数式编程风格
  • 逻辑清晰
  • 可扩展性强(可添加复杂逻辑)

缺点:

  • 性能相对较差(每次都要 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;
}

实现步骤:

  1. 外层遍历原数组
  2. 内层检查结果数组是否已包含当前元素
  3. 不包含则添加到结果数组

优点:

  • 逻辑最直观
  • 不依赖任何内置方法
  • 可完全控制比较逻辑

缺点:

  • 时间复杂度 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;
  });
}

实现步骤:

  1. 创建 Map 记录已出现的元素
  2. 遍历数组,检查 Map 中是否存在
  3. 不存在则记录并保留,存在则过滤

优点:

  • 性能优秀(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));

典型性能排序(从快到慢):

  1. Set 方法 - O(n),最快
  2. Map 方法 - O(n),接近 Set
  3. Filter + indexOf - O(n²),中等规模尚可
  4. Reduce - O(n²),比 filter 稍慢
  5. 双重循环 - O(n²),最慢

五、实际应用建议

  1. 现代项目(支持 ES6+)

    • 优先使用 [...new Set(array)]
    • 简洁、高效、可读性好
  2. 兼容旧浏览器

    • 使用 filter + indexOf
    • 或自己实现兼容版本:
    function unique(arr) {
      if (typeof Set !== 'undefined') {
        return Array.from(new Set(arr));
      }
      return arr.filter((v, i) => arr.indexOf(v) === i);
    }
    
  3. 对象数组去重

    • 使用 Map 记录特定属性
    • 或使用 lodash 的 _.uniqBy
  4. 大数据量场景

    • 必须使用 Set 或 Map
    • 避免 O(n²) 算法
  5. 需要保持顺序

    • Set 和 Map 都保持插入顺序
    • filter + indexOf 保持首次出现顺序

六、总结

数组去重是 JavaScript 基础但重要的操作。选择方法时需要考虑:

  • 数据类型(原始类型 vs 引用类型)
  • 数据规模
  • 浏览器兼容性
  • 是否需要保持顺序
  • 性能要求

对于现代开发,[...new Set(array)] 是最佳选择,兼顾了简洁性、性能和可读性。对于特殊需求,可以根据具体情况选择或实现相应的方法。

JavaScript 中的数组去重方法详解 我们来详细讲解 JavaScript 中数组去重的各种方法,包括它们的原理、优缺点、使用场景和性能差异。 一、问题描述 数组去重是指从包含重复元素的数组中,创建一个新数组,其中每个元素只出现一次。这是 JavaScript 开发中最常见的操作之一,有多种实现方式,每种方式在性能、可读性和适用场景上都有所不同。 二、常用去重方法详解 方法1:使用 Set(ES6+) 这是目前最简洁、最高效的去重方法。 实现步骤: new Set(array) 创建一个 Set 对象 Set 是 ES6 新增的数据结构,它类似于数组,但成员的值都是唯一的 Set 会自动过滤掉重复的值 使用扩展运算符 ... 或 Array.from() 将 Set 转换回数组 优点: 代码简洁,一行搞定 性能优秀,时间复杂度 O(n) 能正确处理所有原始类型(包括 undefined 和 null) 限制: 对于引用类型(对象、数组),是基于引用比较,不是值比较 不兼容旧版浏览器(IE 不支持 Set) 示例对比: 方法2:使用 filter 和 indexOf 这是传统的 ES5 方法,兼容性好。 实现步骤: array.indexOf(item) 返回元素首次出现的索引 只保留索引与首次出现位置相同的元素 通过 filter 创建新数组 工作原理: 第一次遇到元素 2(索引 1): indexOf(2) 返回 1,条件成立,保留 第二次遇到元素 2(索引 2): indexOf(2) 返回 1,条件不成立,过滤 优点: ES5 语法,兼容性好 代码相对简洁 返回新数组,不改变原数组 缺点: 时间复杂度 O(n²),大数组性能差 无法区分 NaN ( indexOf(NaN) 总是返回 -1) 示例: 方法3:使用 reduce 通过 reduce 累积唯一元素。 实现步骤: 初始化为空数组 [] 遍历原数组每个元素 如果累积数组中不存在当前元素,就添加 返回累积数组 优点: 函数式编程风格 逻辑清晰 可扩展性强(可添加复杂逻辑) 缺点: 性能相对较差(每次都要 includes 检查) 同样有 NaN 识别问题 优化版本(使用对象缓存): 方法4:双重循环 最基础的去重算法。 实现步骤: 外层遍历原数组 内层检查结果数组是否已包含当前元素 不包含则添加到结果数组 优点: 逻辑最直观 不依赖任何内置方法 可完全控制比较逻辑 缺点: 时间复杂度 O(n²),性能最差 代码冗长 方法5:使用 Map(ES6+) Map 也可以用于去重,比对象更灵活。 实现步骤: 创建 Map 记录已出现的元素 遍历数组,检查 Map 中是否存在 不存在则记录并保留,存在则过滤 优点: 性能优秀(Map 的 has 和 set 是 O(1)) 可正确处理任何类型(包括对象,基于引用) 保持元素顺序 示例: 三、特殊情况的处理 1. NaN 的处理 NaN 是 JavaScript 中唯一不等于自身的值,需要特殊处理。 解决方案: 2. 对象去重(基于值比较) 默认方法基于引用比较,基于值的比较需要序列化。 基于 JSON.stringify 的方法: 局限性: 无法处理循环引用 属性顺序不同会被视为不同对象 函数、undefined 等会被 JSON.stringify 忽略 更好的方案(使用深度比较): 3. 按特定属性去重 针对对象数组,按某个属性去重。 四、性能比较 通过一个性能测试来比较不同方法: 典型性能排序(从快到慢): Set 方法 - O(n),最快 Map 方法 - O(n),接近 Set Filter + indexOf - O(n²),中等规模尚可 Reduce - O(n²),比 filter 稍慢 双重循环 - O(n²),最慢 五、实际应用建议 现代项目(支持 ES6+) : 优先使用 [...new Set(array)] 简洁、高效、可读性好 兼容旧浏览器 : 使用 filter + indexOf 或自己实现兼容版本: 对象数组去重 : 使用 Map 记录特定属性 或使用 lodash 的 _.uniqBy 大数据量场景 : 必须使用 Set 或 Map 避免 O(n²) 算法 需要保持顺序 : Set 和 Map 都保持插入顺序 filter + indexOf 保持首次出现顺序 六、总结 数组去重是 JavaScript 基础但重要的操作。选择方法时需要考虑: 数据类型(原始类型 vs 引用类型) 数据规模 浏览器兼容性 是否需要保持顺序 性能要求 对于现代开发, [...new Set(array)] 是最佳选择,兼顾了简洁性、性能和可读性。对于特殊需求,可以根据具体情况选择或实现相应的方法。