JavaScript 数组去重方法汇总

小飞兽 JavaScript 475 次阅读 2026-04-24

JavaScript 数组去重方法汇总

一、Introduction(引言)

数组去重是 JavaScript 开发中使用频率极高的基础操作,几乎每个前端工程师在日常编码中都会遇到这一需求。无论是从后端获取的数据需要过滤重复项、用户表单提交的数组需要清洗、还是日常处理日志或统计数据时需要消除重复记录,掌握一套完整、高效的数组去重方法都是必备技能。本文将系统梳理 JavaScript 中去除数组重复元素的各类实现方案,涵盖从最基础的 for 循环到最新的 Set 特性,从一维数组到对象数组的深度去重,从时间复杂度分析到实际业务场景的选择建议,帮助读者在不同需求下快速找到最适合的解决方案。

二、基础语法与核心方法

2.1 Set 去重(最简洁方案)

ES6 引入的 Set 数据结构天然保证元素唯一性,是目前最简洁、运行效率最高的去重方式,适用于基本类型数组。

// 基本用法
const arr = [1, 2, 2, 3, 3, 3, 'apple', 'apple', true, true, NaN, NaN];
const unique = [...new Set(arr)];
console.log(unique);
// 输出: [1, 2, 3, 'apple', true, NaN]
// 注意:Set 认为 NaN 与 NaN 相同,所以只保留一个 NaN
// Array.from + Set(另一种写法)
const unique2 = Array.from(new Set(arr));
console.log(unique2);
// 输出: [1, 2, 3, 'apple', true, NaN]

2.2 indexOf / includes 去重

利用数组的 indexOf()includes() 方法,在遍历时检查新数组中是否已存在该元素。

// indexOf 去重
function uniqueByIndexOf(arr) {
    const result = [];
    for (let i = 0; i < arr.length; i++) {
        if (result.indexOf(arr[i]) === -1) {
            result.push(arr[i]);
        }
    }
    return result;
}

// includes 去重(更语义化,ES7+)
function uniqueByIncludes(arr) {
const result = [];
for (const item of arr) {
if (!result.includes(item)) {
result.push(item);
}
}
return result;
}

const arr = [1, 2, 2, 3, 'hello', 'hello', null, null];
console.log(uniqueByIncludes(arr));
// 输出: [1, 2, 3, 'hello', null]

2.3 filter + indexOf 去重

利用 filter 配合 indexOf,返回元素首次出现的索引等于当前遍历索引的位置,实现优雅的去重。

function uniqueByFilter(arr) {
    return arr.filter((item, index) => arr.indexOf(item) === index);
}

const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'grape'];
console.log(uniqueByFilter(fruits));
// 输出: ['apple', 'banana', 'orange', 'grape']

2.4 reduce + includes 去重

函数式编程风格,将去重逻辑浓缩为一行链式调用。

function uniqueByReduce(arr) {
    return arr.reduce((acc, cur) => {
        return acc.includes(cur) ? acc : [...acc, cur];
    }, []);
}

const nums = [1, 1, 2, 2, 3, 3, 4, 5, 5];
console.log(uniqueByReduce(nums));
// 输出: [1, 2, 3, 4, 5]

2.5 对象键值对去重

利用对象的键不可重复特性,可高效去重,但需要注意类型转换问题(如 1'1' 会被视为同一键)。

function uniqueByObject(arr) {
    const obj = {};
    const result = [];
    for (const item of arr) {
        const key = typeof item + '_' + JSON.stringify(item);
        if (!obj[key]) {
            obj[key] = true;
            result.push(item);
        }
    }
    return result;
}

const mixed = [1, '1', true, 'true', {a:1}, {a:1}];
console.log(uniqueByObject(mixed));
// 输出: [1, '1', true, 'true', {a:1}, {a:1}]
// 注意:对象由于引用不同,即使内容相同也被视为不同元素

2.6 双层 for 循环+splice 去重

传统方法,通过 splice 在原数组上原地删除重复项,适合面试手写实现。

function uniqueBySplice(arr) {
    for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[i] === arr[j]) {
                arr.splice(j, 1);
                j--; // splice 后数组缩短,需调整索引
            }
        }
    }
    return arr;
}

const arr = [1, 2, 2, 3, 3, 3, 4];
console.log(uniqueBySplice([...arr])); // 复制一份再处理
// 输出: [1, 2, 3, 4]

三、代码示例

3.1 基本类型数组去重

// 模拟从后端获取的用户ID列表(含重复)
const userIds = [101, 102, 103, 102, 101, 105, 103, 107, 106, 105, 108];

// 方法1:Set(推荐)
const uniqueIds = [...new Set(userIds)];
console.log('去重后:', uniqueIds);
// 输出: [101, 102, 103, 105, 107, 106, 108]

// 方法2:filter
const uniqueIds2 = userIds.filter((id, idx) => userIds.indexOf(id) === idx);
console.log('filter去重:', uniqueIds2);
// 输出: [101, 102, 103, 105, 107, 106, 108]

3.2 对象数组按指定字段去重

// 订单列表,含重复用户ID的记录
const orders = [
    { id: 1, userId: 'u001', product: 'iPhone', amount: 8999 },
    { id: 2, userId: 'u002', product: 'MacBook', amount: 12999 },
    { id: 3, userId: 'u001', product: 'AirPods', amount: 1299 },
    { id: 4, userId: 'u003', product: 'iPad', amount: 4999 },
    { id: 5, userId: 'u002', product: 'Apple Watch', amount: 2999 },
    { id: 6, userId: 'u001', product: 'Pencil', amount: 799 },
];

// 按 userId 去重,保留每个用户的第一条记录
function uniqueByField(arr, field) {
const seen = new Set();
return arr.filter(item => {
if (seen.has(item[field])) {
return false;
}
seen.add(item[field]);
return true;
});
}

const uniqueOrders = uniqueByField(orders, 'userId');
console.log('按userId去重:');
uniqueOrders.forEach(o => console.log( id=${o.id}, userId=${o.userId}, product=${o.product}));
// 输出:
// id=1, userId=u001, product=iPhone
// id=2, userId=u002, product=MacBook
// id=4, userId=u003, product=iPad

3.3 多字段组合去重

// 同时按 userId 和 product 组合去重
function uniqueByFields(arr, ...fields) {
    const seen = new Set();
    return arr.filter(item => {
        const key = fields.map(f => item[f]).join('|');
        if (seen.has(key)) return false;
        seen.add(key);
        return true;
    });
}

const uniqueByCombo = uniqueByFields(orders, 'userId', 'product');
console.log('按userId+product去重后数量:', uniqueByCombo.length);
// 如果原始没有完全重复的组合,则不删除任何记录

3.4 模糊去重(忽略大小写/空格)

// 字符串数组去重,忽略大小写和首尾空格
const names = [' Alice ', 'Bob', 'ALICE', 'bob ', 'Charlie', 'alice', 'David'];

function uniqueIgnoreCase(arr) {
const seen = new Set();
return arr.filter(item => {
const normalized = item.trim().toLowerCase();
if (seen.has(normalized)) return false;
seen.add(normalized);
return true;
});
}

console.log(uniqueIgnoreCase(names));
// 输出: [' Alice ', 'Bob', 'Charlie', 'David']
// 保留了第一条的原始格式(包含空格)

3.5 复杂对象深度去重(JSON.stringify)

// 通过 JSON.stringify 将对象转为唯一字符串实现深度去重
function uniqueDeep(arr) {
    const seen = new Set();
    return arr.filter(item => {
        const key = JSON.stringify(item);
        if (seen.has(key)) return false;
        seen.add(key);
        return true;
    });
}

const products = [
{ name: 'iPhone', color: 'black' },
{ name: 'iPhone', color: 'black' },
{ name: 'iPhone', color: 'white' },
{ name: { a: 1 }, b: 2 },
{ name: { a: 1 }, b: 2 },
];

console.log('深度去重结果:', uniqueDeep(products).length);
// 输出: 3({name:'iPhone',color:'black'} 被去重一条,{name:'iPhone',color:'white'} 保留,{a:1,b:2} 被去重一条)

四、运行效果

4.1 Set 方案运行结果

const arr = [1, 2, 2, 'hello', 'hello', NaN, null, undefined, undefined];
console.log([...new Set(arr)]);
// [1, 2, 'hello', NaN, null, undefined]
// NaN 被正确识别为相同值,只保留一个
// null 和 undefined 各保留一个

4.2 性能对比测试

// 生成 10000 个随机数(1~1000),统计各方法耗时
const testArr = Array.from({length: 10000}, () => Math.floor(Math.random() * 1000));

console.time('Set');
[...new Set(testArr)];
console.timeEnd('Set');
// Set: ~0.5ms

console.time('filter+indexOf');
testArr.filter((item, idx) => testArr.indexOf(item) === idx);
console.timeEnd('filter+indexOf');
// filter+indexOf: ~800ms(O(n²) 复杂度,数据量大时性能急剧下降)

console.time('reduce+includes');
testArr.reduce((acc, cur) => acc.includes(cur) ? acc : [...acc, cur], []);
console.timeEnd('reduce+includes');
// reduce+includes: ~1200ms(更慢)

console.time('object');
const obj = {};
testArr.filter(v => obj[typeof v + v] ? false : (obj[typeof v + v] = true));
console.timeEnd('object');
// object: ~2ms(效率接近 Set,但实现更复杂)

结论:基本类型去重首选 Set,性能最优且代码简洁。对象数组去重使用 MapSet 配合字段值作为 key,性能远优于 filter+indexOf 的 O(n²) 方案。

4.3 业务场景运行示例

// 场景:从API获取标签列表,去重后渲染前端标签云
const rawTags = ['前端', 'JavaScript', 'Node.js', '前端', 'JavaScript', 'Vue', 'React', 'Node.js', 'CSS', 'JavaScript'];
const tagCloud = [...new Set(rawTags)];
console.log('标签云:', tagCloud);
// ['前端', 'JavaScript', 'Node.js', 'Vue', 'React', 'CSS']

五、常见问题

Q1:为什么 new Set([1, '1']) 会同时保留 1 和 '1'?

Set 使用 SameValueZero 算法比较元素,1(数字)和 '1'(字符串)类型不同,因此被视为不同值都会保留。如果需要忽略类型进行去重,应手动在添加到 Set 前统一类型(如转为字符串)。

// 忽略类型去重
const arr = [1, '1', 2, '2', 3];
const result = [...new Set(arr.map(v => String(v)))].map(v => Number(v));
console.log(result); // [1, 2, 3]

Q2:indexOf 无法正确识别 NaN,如何处理?

Array.prototype.indexOf() 使用严格相等(===),而 NaN === NaN 返回 false,导致 NaN 无法被正确去重识别。解决方案是使用 Set 或自定义比较函数。

const arr = [NaN, 1, NaN, 2, NaN];

// filter+indexOf 方案:NaN 不会被识别为重复
console.log(arr.filter((v, i) => arr.indexOf(v) === i));
// [NaN, 1, 2] —— NaN 出现了多次(indexOf 每次都返回 -1)

// Set 方案:NaN 被正确去重
console.log([...new Set(arr)]);
// [NaN, 1, 2] —— 正确

Q3:对象数组去重使用 JSON.stringify 有什么隐患?

JSON.stringify 对属性的顺序敏感,{a:1, b:2}{b:2, a:1} 序列化结果不同,会被视为不同对象。此外,函数(undefined)会在序列化过程中被忽略或报错。处理复杂对象建议使用自定义哈希函数或 Map 配合字段比较。

// 隐患示例
const obj1 = {a: 1, b: 2};
const obj2 = {b: 2, a: 1};
console.log(JSON.stringify(obj1) === JSON.stringify(obj2)); // true(意外相同)

// 但对于有序属性不同的对象
const obj3 = {a: 1, b: {x: 1}};
const obj4 = {a: 1, b: {y: 2}};
console.log(JSON.stringify(obj3) === JSON.stringify(obj4)); // false(正常)

Q4:如何保留重复项中最后一条记录而不是第一条?

filter 中比较逻辑改为 indexOf 改为 lastIndexOf,即可保留最后一条。

function uniqueKeepLast(arr) {
    return arr.filter((item, index) => {
        return arr.lastIndexOf(item) === index;
    });
}
const arr = [1, 2, 2, 3, 3, 3];
console.log(uniqueKeepLast(arr)); // [1, 2, 3](和保留第一条结果相同,仅顺序不同场景有区别)

Q5:Map 能否用于去重?与 Set 有何区别?

Map 和 Set 都能实现去重,区别在于 Map 适用于「键值对」场景(可以保留额外信息),Set 适用于单一值去重。对于对象数组按字段去重,用 Map 以字段值为 key 最为高效。

// Map 去重(保留最后一条,并附带额外信息)
function uniqueWithMap(arr, key) {
    const map = new Map();
    arr.forEach(item => map.set(item[key], item)); // 后面的覆盖前面的
    return [...map.values()];
}

// Set 去重(最简洁值去重)
function uniqueWithSet(arr) {
return [...new Set(arr)];
}

六、延伸阅读