数组去重
数组去重(Unique / Deduplication)指从数组中移除“重复元素”。关键点不在于“怎么删”,而在于:
- 你如何定义“重复”?(值相等 / 引用相同 / 某些字段相同 / 自定义比较)
- 你是否需要保留原始顺序?(一般去重保留第一次出现的元素)
下面按常见场景整理:基础类型、对象按字段、联合字段、自定义比较,并补充复杂度与坑点。
1) 基础类型去重:Set
适用:number/string/boolean/null/undefined/symbol/bigint 等“可直接比较的值”。
复杂度
- 时间:
- 空间:
关键语义(很容易考)
JavaScript 的 Set 使用 SameValueZero 语义:
NaN与NaN视为相等(能正确去掉多个NaN)+0与-0视为相等- 对象/数组/函数等“引用类型”,只有在引用相同(同一个对象)时才算重复
export function uniquePrimitive<T>(arr: T[]): T[] {
return [...new Set(arr)];
}
uniquePrimitive([1, 1, 2, NaN, NaN]); // [1, 2, NaN]
uniquePrimitive([{ a: 1 }, { a: 1 }]); // 不会去重(引用不同)2) 对象数组按单字段去重:Map/Set + key
适用:数组元素为对象,按某个字段(如 id)去重。
- 时间:
- 空间:
/**
* 对象数组按字段去重(保留第一次出现的项)
*/
export function uniqueByKey<T extends Record<string, any>>(
arr: T[],
key: keyof T
): T[] {
const seen = new Set<any>();
const result: T[] = [];
for (const item of arr) {
const value = item[key];
if (!seen.has(value)) {
seen.add(value);
result.push(item);
}
}
return result;
}
const users = [
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 },
{ id: 1, name: '张三(重复)', age: 26 }, // id 重复
{ id: 3, name: '王五', age: 28 }
];
uniqueByKey(users, 'id');
// => [{id:1,...},{id:2,...},{id:3,...}]3) 联合字段去重:推荐 keyFn(更通用)
适用:需要用多个字段一起判定重复,比如 userId + productId。
为什么不建议直接 join('|')
keys.map(k => item[k]).join('|') 可能产生“拼接碰撞”,例如:
['a|b', 'c']与['a', 'b|c']拼起来都可能是a|b|c
更稳妥的方式:
- 用
keyFn直接返回唯一 key(你自己保证唯一) - 或对 key 数组做结构化序列化(如
JSON.stringify([v1, v2]))
/**
* 通用去重:用 keyFn 决定“重复”的判定 key
*/
export function uniqueBy<T, K>(arr: T[], keyFn: (item: T) => K): T[] {
const seen = new Set<any>();
const result: T[] = [];
for (const item of arr) {
const key = keyFn(item);
if (!seen.has(key)) {
seen.add(key);
result.push(item);
}
}
return result;
}
type Order = {
userId: number;
productId: string;
date: string;
amount: number;
};
const orders: Order[] = [
{ userId: 100, productId: 'P001', date: '2024-01-01', amount: 199 },
{ userId: 100, productId: 'P002', date: '2024-01-01', amount: 299 },
{ userId: 100, productId: 'P001', date: '2024-01-02', amount: 199 }, // 联合字段重复,但 date 不同
{ userId: 100, productId: 'P001', date: '2024-01-01', amount: 189 },
{ userId: 101, productId: 'P001', date: '2024-01-01', amount: 199 }
];
// 按 userId + productId 联合去重
const uniqueOrders = uniqueBy(orders, (o) => JSON.stringify([o.userId, o.productId]));4) 自定义比较去重:findIndex + compareFn(最灵活但最慢)
适用:复杂判等(例如深比较、近似相等、忽略大小写、忽略空格等)。
注意:这种方式通常是 ,数据量大时不建议。
export function uniqueByCompare<T>(
arr: T[],
compareFn: (a: T, b: T) => boolean
): T[] {
return arr.filter((item, index, self) => {
return index === self.findIndex((t) => compareFn(t, item));
});
}
// 示例:忽略字符串大小写去重
uniqueByCompare(['A', 'a', 'B'], (a, b) => String(a).toLowerCase() === String(b).toLowerCase());
// => ['A','B']常见坑点总结
- 引用类型不会被
Set自动去重:[{a:1},{a:1}]视为不同。 NaN可以被Set去重:这是 SameValueZero 语义带来的好处。- 联合字段拼接可能碰撞:尽量用
keyFn或结构化序列化。 - 是否保序:上述实现都保留“第一次出现的元素”,顺序稳定。
- 大数据量优先 解法:
compareFn的 只适合小数组。