JavaScript 数组去重有多种方法,性能差异显著。
以下是常用方法及其性能对比:
1. ES6 Set(推荐)
javascript
// 最快,代码最简洁 function uniqueSet(arr) { return [...new Set(arr)]; // 或 Array.from(new Set(arr)) }2. Filter + indexOf
javascript
function uniqueFilter(arr) { return arr.filter((item, index) => arr.indexOf(item) === index); }3. Reduce
javascript
function uniqueReduce(arr) { return arr.reduce((acc, cur) => { if (!acc.includes(cur)) { acc.push(cur); } return acc; }, []); }4. 双层循环
javascript
function uniqueDoubleLoop(arr) { const result = []; for (let i = 0; i < arr.length; i++) { let isDuplicate = false; for (let j = 0; j < result.length; j++) { if (arr[i] === result[j]) { isDuplicate = true; break; } } if (!isDuplicate) result.push(arr[i]); } return result; }5. Object键值(仅字符串)
javascript
function uniqueObject(arr) { const obj = {}; const result = []; for (let i = 0; i < arr.length; i++) { if (!obj[arr[i]]) { obj[arr[i]] = true; result.push(arr[i]); } } return result; }性能对比测试
javascript
// 生成测试数据 const testArray = []; for (let i = 0; i < 10000; i++) { testArray.push(Math.floor(Math.random() * 1000)); } // 性能测试函数 function testPerformance(fn, arr, name) { const start = performance.now(); fn([...arr]); // 避免修改原数组 const end = performance.now(); console.log(`${name}: ${(end - start).toFixed(2)}ms`); } // 测试 console.log('10000个元素的数组去重性能对比:'); testPerformance(uniqueSet, testArray, 'Set'); testPerformance(uniqueFilter, testArray, 'Filter'); testPerformance(uniqueReduce, testArray, 'Reduce'); testPerformance(uniqueDoubleLoop, testArray, '双层循环'); testPerformance(uniqueObject, testArray, 'Object键值');性能排名(从快到慢)
Set- O(n) - 最快,代码最简洁
Object键值- O(n) - 较快,但仅适用于字符串或可转字符串的值
Filter + indexOf- O(n²) - 中等,代码简洁但性能随数据量下降
Reduce- O(n²) - 中等偏慢
双层循环- O(n²) - 最慢
特殊场景考虑
处理复杂对象
javascript
// 对象数组去重(基于某个属性) function uniqueByKey(arr, key) { const map = new Map(); return arr.filter(item => { if (!map.has(item[key])) { map.set(item[key], true); return true; } return false; }); } // 使用JSON.stringify(性能较差) function uniqueObjects(arr) { const set = new Set(); return arr.filter(obj => { const str = JSON.stringify(obj); if (set.has(str)) return false; set.add(str); return true; }); }保持顺序
大多数方法(Set、Filter、Reduce)都能保持原始顺序,但Object键值不保证顺序。
总结建议
| 方法 | 推荐度 | 适用场景 |
|---|---|---|
| Set | ⭐⭐⭐⭐⭐ | 绝大多数场景,ES6环境 |
| Object键值 | ⭐⭐⭐ | 简单值且不关心类型转换 |
| Filter | ⭐⭐ | 小数组,简洁代码优先 |
| Reduce | ⭐⭐ | 需要复杂处理逻辑 |
| 双层循环 | ⭐ | 学习算法时,实际开发不推荐 |
最佳实践:
现代项目直接使用
[...new Set(arr)]需要考虑兼容性时,使用polyfill或lodash的
_.uniq()大型数组优先考虑时间复杂度O(n)的方法
扩展运算符(Spread Operator)...总结
| 类别 | 语法 | 描述 | 示例 |
|---|---|---|---|
| 基本概念 | ...iterable | 将可迭代对象展开为独立元素 | ...array,...string |
| 数组操作 | |||
| 复制数组 | const newArr = [...arr] | 浅拷贝数组 | const arr1 = [1,2,3]; const arr2 = [...arr1] |
| 合并数组 | [...arr1, ...arr2] | 合并多个数组 | [1,2, ...arr, 4,5] |
| 数组去重 | [...new Set(arr)] | 配合Set快速去重 | [...new Set([1,2,2,3])] // [1,2,3] |
| 插入元素 | [first, ...rest] | 结合解构分离首元素 | const [first, ...others] = [1,2,3,4] |
| 对象操作 | |||
| 复制对象 | const newObj = {...obj} | 浅拷贝对象属性 | const obj1 = {a:1}; const obj2 = {...obj1} |
| 合并对象 | {...obj1, ...obj2} | 合并对象,后者覆盖前者 | {...defaults, ...options} |
| 添加属性 | {...obj, newProp: value} | 添加新属性 | {...user, age: 25} |
| 覆盖属性 | {...obj, prop: newValue} | 修改已有属性值 | {...user, name: 'Bob'} |
| 函数应用 | |||
| 函数参数 | func(...args) | 将数组展开为函数参数 | Math.max(...[1,2,3]) |
| 剩余参数 | function(...args){} | 收集剩余参数为数组 | function sum(...nums){...} |
| 字符串操作 | |||
| 字符串转数组 | [...str] | 正确处理Unicode字符 | [...'hello'] // ['h','e','l','l','o'] |
| 表情符号处理 | [...'👨👩👧'] | 正确处理组合emoji | [...'👍🏽'] // ['👍🏽'] |
| 可迭代对象 | |||
| NodeList转数组 | [...document.querySelectorAll('div')] | 类数组转真正数组 | const divs = [...document.querySelectorAll('div')] |
| Arguments转数组 | [...arguments] | 函数arguments转数组 | function f(){ return [...arguments]; } |
| Map/Set转数组 | [...map],[...set] | Map/Set转为数组 | [...new Set([1,2,2])] // [1,2] |
| 组合使用 | |||
| 数组+解构 | [first, ...rest] = arr | 提取首元素和剩余数组 | const [head, ...tail] = [1,2,3,4] |
| 对象+解构 | {a, ...rest} = obj | 提取指定属性和剩余属性 | const {id, ...data} = obj |
| 嵌套展开 | [...arr1, mid, ...arr2] | 在任意位置展开数组 | [0, ...middle, 9] |
| 注意事项 | |||
| 浅拷贝 | {...obj},[...arr] | 只拷贝一层,嵌套对象引用共享 | {...obj}不会深拷贝嵌套对象 |
| 不可迭代错误 | ...123 | 数字等不可迭代对象会报错 | ...123 // TypeError |
| 性能考虑 | 大数组展开 | 大数组展开可能影响性能 | 避免在循环中频繁展开大数组 |
| 特殊技巧 | |||
| 条件展开 | {...obj, ...(cond && {prop: val})} | 条件添加属性 | {...obj, ...(show && {visible: true})} |
| 移除属性 | {propToRemove, ...rest} = obj | 结合解构移除属性 | const {password, ...safeUser} = user |
| 默认值+覆盖 | {...defaults, ...overrides} | 设置默认值并允许覆盖 | const config = {...defaultConfig, ...userConfig} |
关键特点总结
浅拷贝:只复制一层,嵌套对象是引用
可迭代性:只能用于可迭代对象(数组、字符串、Set、Map等)
不可变操作:创建新对象/数组,不修改原数据
顺序重要:在对象合并中,后面的属性覆盖前面的
ES6+特性:需要Babel转译以支持旧浏览器
实用示例
javascript
// 1. 快速数组合并去重 const uniqueMerge = [...new Set([...arr1, ...arr2])]; // 2. 带默认值的对象配置 const createConfig = (userConfig) => ({ port: 3000, host: 'localhost', ...userConfig }); // 3. 安全的删除属性 const removePassword = ({password, ...user}) => user; // 4. 数组插入元素 const insertAt = (arr, index, ...items) => [ ...arr.slice(0, index), ...items, ...arr.slice(index) ];扩展运算符是现代JavaScript开发中最常用、最简洁的特性之一,极大简化了数组和对象的操作。