js 数组去重(手把手讲解)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目:《Spring AI 项目实战》 正在持续爆肝中,基于 Spring AI + Spring Boot 3.x + JDK 21..., 点击查看 ;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

在 JavaScript 开发中,数组去重是一个基础却重要的操作。无论是处理用户输入的数据、过滤重复的 API 响应,还是优化数据展示逻辑,开发者都需要掌握数组去重的核心方法。对于编程初学者而言,这一问题既简单又容易陷入误区;而对中级开发者来说,如何选择高效且可维护的解决方案则是关键。本文将从零开始,通过通俗易懂的比喻、代码示例和性能分析,系统讲解多种 JavaScript 数组去重方法,并结合实际案例帮助读者深入理解其原理与适用场景。


一、基础方法:循环遍历与条件判断

1.1 数组去重的核心逻辑

数组去重的本质是“保留唯一值,移除重复项”。想象你正在整理书架,每本书都有一个唯一的 ISBN 号码。当你发现两本相同 ISBN 的书时,只需保留一本即可。在 JavaScript 中,数组去重的逻辑与此类似:遍历数组中的每个元素,并确保每个元素只出现一次。

1.2 传统 for 循环实现

最基础的方法是通过 for 循环结合条件判断实现:

function uniqueArray(arr) {  
  const result = [];  
  for (let i = 0; i < arr.length; i++) {  
    if (!result.includes(arr[i])) {  
      result.push(arr[i]);  
    }  
  }  
  return result;  
}  
// 示例:  
const arr = [1, 2, 2, 3, 4, 4, 5];  
console.log(uniqueArray(arr)); // [1, 2, 3, 4, 5]  

原理说明

  • 创建一个空数组 result 作为结果容器。
  • 遍历原始数组的每个元素,若 result 中尚未包含该元素,则将其加入 result
  • 最终返回去重后的数组。

1.3 问题与改进

上述方法存在两个潜在问题:

  1. 时间复杂度高includes() 方法本身需要遍历 result 数组,导致整体时间复杂度为 O(n²),当处理大数据量时性能较差。
  2. 无法处理非原始值:例如对象或数组元素的去重,因为 includes() 会基于严格相等(===)比较,而对象引用地址不同会被视为不同元素。

改进方案

// 使用对象作为哈希表优化性能  
function uniqueArrayOptimized(arr) {  
  const seen = {};  
  const result = [];  
  for (let i = 0; i < arr.length; i++) {  
    const item = arr[i];  
    if (!seen[item]) {  
      seen[item] = true;  
      result.push(item);  
    }  
  }  
  return result;  
}  

通过对象 seen 存储已出现的元素,利用对象键的唯一性特性,将时间复杂度降低到 O(n)。


二、进阶方法:ES6 新特性与高阶函数

2.1 Set 数据结构

ES6 引入的 Set 对象天生具备去重功能,其核心特性是“值的集合,且元素唯一”。

const arr = [1, 2, 2, 3, 4, 4, 5];  
const unique = [...new Set(arr)];  
console.log(unique); // [1, 2, 3, 4, 5]  

优势

  • 代码简洁,一行解决去重问题。
  • 时间复杂度为 O(n),性能高效。

注意事项

  • Set 仅适用于可遍历的值(如数字、字符串等原始值)。若需处理对象数组,需结合其他方法。
  • 需注意浏览器兼容性,确保目标环境支持 ES6。

2.2 Array.prototype.filter() 与 includes() 结合

const unique = arr => arr.filter((item, index) => arr.indexOf(item) === index);  

或使用 includes

const unique = arr => arr.filter((item, index) => !arr.includes(item, index + 1));  

原理

  • filter() 方法遍历数组,对每个元素判断其首次出现的位置是否为当前索引。
  • 第二种方法通过 includes 检查当前元素是否在后续位置重复出现。

性能对比
| 方法 | 时间复杂度 | 代码简洁性 | 兼容性 |
|---------------------|------------|------------|-------------|
| for 循环 + 哈希表 | O(n) | 中等 | 全平台 |
| Set | O(n) | 高 | 需 ES6 |
| filter + indexOf | O(n²) | 中等 | 全平台 |
| filter + includes | O(n²) | 中等 | 全平台 |


三、高级技巧:对象与 Map 的应用

3.1 对象键存储法

对于非 ES6 环境或需兼容旧版本的场景,可通过对象键的唯一性实现去重:

function uniqueWithObject(arr) {  
  const temp = {};  
  return arr.filter(item => {  
    const key = typeof item === 'object' ? JSON.stringify(item) : item;  
    return temp[key] ? false : (temp[key] = true);  
  });  
}  
// 示例:处理对象数组  
const arr = [  
  { id: 1 },  
  { id: 1 },  
  { id: 2 }  
];  
console.log(uniqueWithObject(arr)); // 只保留第一个和第三个对象  

关键点

  • 将对象转为 JSON 字符串作为键,解决对象引用地址不同的问题。

3.2 Map 结构优化

Map 可以存储键值对,并利用键的唯一性实现去重:

function uniqueWithMap(arr) {  
  const map = new Map();  
  return arr.filter(item => {  
    const key = JSON.stringify(item);  
    if (!map.has(key)) {  
      map.set(key, true);  
      return true;  
    }  
    return false;  
  });  
}  

此方法在处理复杂对象时更灵活,但需要 ES6 支持。


四、算法实现:递归与分治策略

4.1 递归法去重

通过递归不断缩小问题规模,逐步去除重复项:

function uniqueRecursive(arr) {  
  if (arr.length <= 1) return arr;  
  const [first, ...rest] = arr;  
  return [  
    first,  
    ...uniqueRecursive(rest.filter(item => item !== first))  
  ];  
}  

缺点

  • 时间复杂度为 O(n²),递归深度过大会导致栈溢出。

4.2 分治法去重

将数组拆分为子数组,分别去重后再合并:

function divideAndConquer(arr) {  
  if (arr.length <= 1) return arr;  
  const mid = Math.floor(arr.length / 2);  
  const left = divideAndConquer(arr.slice(0, mid));  
  const right = divideAndConquer(arr.slice(mid));  
  const combined = [...left, ...right];  
  // 合并后去重  
  return [...new Set(combined)];  
}  

此方法结合了分治策略与 Set 去重,但实际性能可能不如直接使用 Set。


五、性能优化与场景选择

5.1 时间复杂度对比

方法时间复杂度适用场景
for 循环 + 哈希表O(n)大数据量,兼容性要求高
Set 转数组O(n)现代浏览器,简洁代码优先
filter + indexOfO(n²)小数据量,无需优化

5.2 实际测试案例

假设有一个包含 10,000 个元素的数组(含大量重复值),通过以下代码测试不同方法的执行时间:

const arr = Array.from({ length: 10000 }, () => Math.floor(Math.random() * 1000));  

console.time('Set method');  
[...new Set(arr)];  
console.timeEnd('Set method'); // ~0.1ms  

console.time('Hash method');  
const hash = {};  
arr.forEach(item => { hash[item] = true; });  
Object.keys(hash).map(Number);  
console.timeEnd('Hash method'); // ~0.1ms  

console.time('Filter method');  
arr.filter((item, index) => arr.indexOf(item) === index);  
console.timeEnd('Filter method'); // ~50ms  

结论:基于哈希表或 Set 的方法在大数据量下性能显著优于传统方法。


六、常见问题与解决方案

6.1 处理多维数组或对象

当需要去重的对象或嵌套数组时,需先序列化为字符串:

const arr = [  
  { name: 'Alice', age: 20 },  
  { name: 'Bob', age: 25 },  
  { name: 'Alice', age: 20 }  
];  

const unique = arr => {  
  const seen = new Set();  
  return arr.filter(item => {  
    const key = JSON.stringify(item);  
    return seen.has(key) ? false : seen.add(key);  
  });  
};  

console.log(unique(arr)); // 只保留前两个对象  

6.2 保留首次出现的元素

多数方法默认保留首次出现的元素,但若需保留最后一次出现的元素,可调整逻辑:

function keepLast(arr) {  
  const seen = {};  
  const result = [];  
  for (const item of arr) {  
    seen[item] = item; // 覆盖前值  
  }  
  return Object.values(seen);  
}  

结论

JavaScript 数组去重是一个看似简单却充满细节的课题。开发者需要根据具体场景选择合适的方法:

  • 基础场景:使用 Set 或哈希表实现高效去重。
  • 兼容性要求高:采用对象键存储法或优化后的 for 循环。
  • 复杂对象处理:结合 JSON 序列化与 Map 结构。

通过理解不同方法的底层原理与性能差异,开发者可以编写出既简洁又高效的代码。记住,没有“万能”的去重方案,选择始终取决于数据特性、代码环境与性能需求。希望本文能为你提供清晰的思路与实用的解决方案!

最新发布