JavaScript 数组去重方法大全

66 阅读4分钟

1. 使用 Set(ES6+ 推荐)

最简单、最优雅的方法

const arr = [1, 2, 2, 3, 4, 4, 5, 1, 6];

// 方法1:Set + 展开运算符
const unique1 = [...new Set(arr)];
console.log(unique1); // [1, 2, 3, 4, 5, 6]

// 方法2:Set + Array.from
const unique2 = Array.from(new Set(arr));
console.log(unique2); // [1, 2, 3, 4, 5, 6]

优点:代码简洁,性能好 缺点:无法处理特殊对象(但,NaN、+0/-0 等会被正确处理)

2. 使用 filter + indexOf

经典方法,兼容性好

const arr = [1, 2, 2, 3, 4, 4, 5, 1, 6];

// 只保留第一个出现的元素
const unique = arr.filter((item, index) => {
    return arr.indexOf(item) === index;
});

console.log(unique); // [1, 2, 3, 4, 5, 6]

// 一行写法
const uniqueOneLine = arr.filter((item, index) => arr.indexOf(item) === index);

时间复杂度:O(n²),大数组性能较差 注意:indexOf 使用严格相等(===),NaN 无法被正确识别

3. 使用 reduce

功能强大,适合复杂去重逻辑

const arr = [1, 2, 2, 3, 4, 4, 5, 1, 6];

// 基本去重
const unique = arr.reduce((accumulator, currentValue) => {
    if (!accumulator.includes(currentValue)) {
        accumulator.push(currentValue);
    }
    return accumulator;
}, []);

console.log(unique); // [1, 2, 3, 4, 5, 6]

// 使用对象加速查找
const uniqueFast = arr.reduce((acc, cur) => {
    if (!acc.seen[cur]) {
        acc.seen[cur] = true;
        acc.result.push(cur);
    }
    return acc;
}, { seen: {}, result: [] }).result;

console.log(uniqueFast); // [1, 2, 3, 4, 5, 6]

4. 使用 for 循环

最基础的方法,可控性强

const arr = [1, 2, 2, 3, 4, 4, 5, 1, 6];

function unique(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;
}

console.log(unique(arr)); // [1, 2, 3, 4, 5, 6]

// 优化版:使用对象缓存
function uniqueOptimized(arr) {
    const result = [];
    const seen = {};
    
    for (let i = 0; i < arr.length; i++) {
        const item = arr[i];
        // 使用 typeof 处理特殊键名
        const key = typeof item + JSON.stringify(item);
        
        if (!seen[key]) {
            seen[key] = true;
            result.push(item);
        }
    }
    
    return result;
}

5. 使用 includes(ES7+)

语法更直观

const arr = [1, 2, 2, 3, 4, 4, 5, 1, 6];

const unique = [];
for (let item of arr) {
    if (!unique.includes(item)) {
        unique.push(item);
    }
}

console.log(unique); // [1, 2, 3, 4, 5, 6]

// 一行箭头函数版本
const uniqueFn = arr => arr.reduce((acc, cur) => 
    acc.includes(cur) ? acc : [...acc, cur], []);

6. 使用 Map(ES6)

保持元素插入顺序

const arr = [1, 2, 2, 3, 4, 4, 5, 1, 6];

// 使用 Map
const unique = [...new Map(arr.map(item => [item, item])).values()];
console.log(unique); // [1, 2, 3, 4, 5, 6]

// 更清晰的写法
function uniqueByMap(arr) {
    const map = new Map();
    const result = [];
    
    for (const item of arr) {
        if (!map.has(item)) {
            map.set(item, true);
            result.push(item);
        }
    }
    
    return result;
}

7. 处理特殊情况的去重

对象数组去重

const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 1, name: 'Alice' }, // 重复
    { id: 3, name: 'Charlie' },
    { id: 2, name: 'Bob' }    // 重复
];

// 方法1:使用 Map 根据特定属性去重
const uniqueById = [
    ...new Map(users.map(user => [user.id, user])).values()
];
console.log(uniqueById); // 3个对象,id 为 1, 2, 3

// 方法2:使用 Set 和 JSON.stringify(效率较低)
const uniqueUsers = Array.from(
    new Set(users.map(user => JSON.stringify(user)))
).map(str => JSON.parse(str));
console.log(uniqueUsers); // 3个对象

// 方法3:自定义比较函数
function uniqueBy(arr, keyFn) {
    const seen = new Set();
    return arr.filter(item => {
        const key = keyFn(item);
        if (seen.has(key)) {
            return false;
        }
        seen.add(key);
        return true;
    });
}

const uniqueByName = uniqueBy(users, user => user.name);

处理 NaN

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

// Set 可以正确处理 NaN
const uniqueWithSet = [...new Set(arr)];
console.log(uniqueWithSet); // [1, 2, NaN, 3]

// 自定义处理 NaN
function uniqueWithNaN(arr) {
    const seen = new Set();
    const hasNaN = arr.some(item => isNaN(item) && typeof item === 'number');
    
    return arr.filter(item => {
        if (isNaN(item) && typeof item === 'number') {
            if (hasNaN && !seen.has('NaN')) {
                seen.add('NaN');
                return true;
            }
            return false;
        }
        if (!seen.has(item)) {
            seen.add(item);
            return true;
        }
        return false;
    });
}

8. 性能对比

// 性能测试函数
function performanceTest(method, arr, iterations = 1000) {
    const start = performance.now();
    
    for (let i = 0; i < iterations; i++) {
        method([...arr]); // 避免修改原数组
    }
    
    const end = performance.now();
    return end - start;
}

// 测试数据
const testData = Array.from({length: 1000}, (_, i) => i % 100);

// 各种方法
const methods = {
    'Set': arr => [...new Set(arr)],
    'filter + indexOf': arr => arr.filter((v, i, a) => a.indexOf(v) === i),
    'reduce': arr => arr.reduce((acc, cur) => 
        acc.includes(cur) ? acc : [...acc, cur], []),
    'Map': arr => [...new Map(arr.map(v => [v, v])).values()],
    'for + includes': arr => {
        const result = [];
        for (let v of arr) {
            if (!result.includes(v)) result.push(v);
        }
        return result;
    }
};

// 运行测试
Object.entries(methods).forEach(([name, method]) => {
    const time = performanceTest(method, testData, 1000);
    console.log(`${name}: ${time.toFixed(2)}ms`);
});

// 典型结果(1000个元素,1000次迭代):
// Set: ~5ms
// Map: ~8ms
// filter + indexOf: ~120ms
// reduce: ~150ms
// for + includes: ~180ms

9. 综合封装函数

/**
 * 通用数组去重函数
 * @param {Array} arr - 要去重的数组
 * @param {Function} [keyFn] - 生成唯一标识的函数
 * @param {boolean} [keepOrder=true] - 是否保持原顺序
 * @returns {Array} 去重后的数组
 */
function unique(arr, keyFn, keepOrder = true) {
    if (!Array.isArray(arr)) {
        throw new TypeError('Expected an array');
    }
    
    // 默认使用 Set(最快)
    if (!keyFn && keepOrder) {
        return [...new Set(arr)];
    }
    
    if (keyFn) {
        // 根据 keyFn 去重
        const seen = new Set();
        return arr.filter(item => {
            const key = keyFn(item);
            if (seen.has(key)) {
                return false;
            }
            seen.add(key);
            return true;
        });
    }
    
    if (!keepOrder) {
        // 不保持顺序,可以使用更快的算法
        return arr.filter((item, index) => arr.indexOf(item) === index);
    }
    
    // 通用方法
    const result = [];
    const seen = new Set();
    
    for (const item of arr) {
        if (!seen.has(item)) {
            seen.add(item);
            result.push(item);
        }
    }
    
    return result;
}

// 使用示例
console.log(unique([1, 2, 2, 3, 3, 3])); // [1, 2, 3]

// 对象数组去重
const objects = [{x: 1}, {x: 2}, {x: 1}];
console.log(unique(objects, obj => obj.x)); // [{x: 1}, {x: 2}]

总结与选择建议

方法优点缺点适用场景
Set简洁、性能最好、自动处理 NaNES6+ 支持大部分场景首选
filter + indexOf兼容性好、代码简洁性能差(O(n²))小数组、需要兼容老浏览器
reduce灵活、可处理复杂逻辑代码稍复杂需要自定义去重逻辑
Map保持顺序、性能好代码稍复杂需要保持顺序的场景
for 循环最基础、可控性强代码冗长教学、需要最大控制权

最佳实践建议

  1. 现代项目:使用 Set(最简单高效)
  2. 需要兼容性:使用 filter + indexOf​ 或 polyfill
  3. 对象数组:使用 Map​ 或自定义 key 函数
  4. 性能关键:先测试,不同场景最优解可能不同
  5. 大型数组:避免使用 O(n²) 算法