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 | 简洁、性能最好、自动处理 NaN | ES6+ 支持 | 大部分场景首选 |
| filter + indexOf | 兼容性好、代码简洁 | 性能差(O(n²)) | 小数组、需要兼容老浏览器 |
| reduce | 灵活、可处理复杂逻辑 | 代码稍复杂 | 需要自定义去重逻辑 |
| Map | 保持顺序、性能好 | 代码稍复杂 | 需要保持顺序的场景 |
| for 循环 | 最基础、可控性强 | 代码冗长 | 教学、需要最大控制权 |
最佳实践建议:
- 现代项目:使用 Set(最简单高效)
- 需要兼容性:使用 filter + indexOf 或 polyfill
- 对象数组:使用 Map 或自定义 key 函数
- 性能关键:先测试,不同场景最优解可能不同
- 大型数组:避免使用 O(n²) 算法