Appearance
✨ 数组去重 👌
要点速览
- 基本类型优先用
Set,一行搞定,时间/空间均为O(n),且能正确处理NaN。 - 对象数组用
Map按字段去重(如id),避免对象键强制字符串化的陷阱。 - 多字段与自定义比较可抽象为
unique(arr, { key: [...], compare }),提升工程可扩展性。 - 慎用深度去重:
JSON.stringify受属性顺序影响;需要先归一化键顺序。 - 明确需求维度:数据类型、顺序要求、对比深度与性能指标,先界定再落实现。
快速上手
最常用的两段代码,覆盖 80% 场景:
js
// 基本类型去重:数字、字符串、Symbol、NaN 等
const uniqueBasic = (arr) => [...new Set(arr)];
// 对象数组按字段去重:如按 id
const uniqueBy = (arr, key) => {
const map = new Map();
return arr.filter((item) => !map.has(item[key]) && map.set(item[key], true));
};
// 示例
uniqueBasic([1, 2, 2, NaN, NaN]); // => [1, 2, NaN]
uniqueBy(
[
{ id: 1, name: "a" },
{ id: 1, name: "b" },
{ id: 2, name: "c" },
],
"id"
); // => [{ id:1, name:"a" }, { id:2, name:"c" }]边界与复杂度
Set/Map方案时间/空间复杂度均为O(n);双重循环是O(n^2),仅适用于小数组或兼容性受限环境。- 稳定性:上面两种实现均“保留首个出现的元素”,保持原始顺序稳定。
问题拆解维度
在解决数组去重问题前,需从以下 4 个核心维度明确需求:
- 数据类型:区分基本类型(数字、字符串、Symbol、NaN)与引用类型(对象数组)
- 顺序要求:是否需要保留数组元素的原始顺序
- 对比深度:对象数组去重是按特定字段(如 id)对比,还是全属性深度对比
- 性能指标:评估不同方案的时间复杂度(O(n)、O(n²))与空间复杂度(O(n))
核心去重方法详解
基本类型去重
ES6 Set 方法(推荐)
- 原理:利用
Set“天然不存储重复值”的特性,结合展开运算符实现转换 - 优势:
- 简洁高效,一行代码实现
- 自动处理
NaN(Set内部将NaN视为与自身相等,解决NaN !== NaN的语言特性问题) - 时间/空间复杂度均为
O(n),适配多数业务场景
- 适用场景:无引用类型的纯基本类型数组
示例实现:
javascript
// 基本类型去重(保序、可处理 NaN)
function uniqueBasic(arr) {
return [...new Set(arr)];
}
console.log(uniqueBasic([1, 2, 2, NaN, NaN, "a"])); // [1, 2, NaN, 'a']传统方法(无 ES6 环境)
| 方法 | 实现逻辑 | 复杂度 | 缺陷 |
|---|---|---|---|
双重循环+indexOf | 外层遍历原数组,内层用indexOf检查元素是否在新数组中,无则 push | 时间 O(n²)、空间 O(n) | 性能差,不适用于大数组,且无法处理 NaN |
| 对象哈希表优化 | 创建空对象,以数组元素为键存储,避免重复键值 | 时间 O(n)、空间 O(n) | 对象键会强制转为字符串,导致1与"1"被判定为重复 |
对应代码实现:
javascript
// 1) 双重循环 + indexOf(不处理 NaN)
function uniqueIndexOf(arr) {
const res = [];
for (let i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) === -1) {
res.push(arr[i]);
}
}
return res;
}
console.log(uniqueIndexOf([1, 2, 2, NaN, NaN])); // [1, 2, NaN, NaN]javascript
// 2) includes 版本(可处理 NaN,ES7+)
function uniqueIncludes(arr) {
const res = [];
for (const x of arr) {
if (!res.includes(x)) res.push(x);
}
return res;
}
console.log(uniqueIncludes([1, 2, 2, NaN, NaN])); // [1, 2, NaN]javascript
// 3) 对象哈希表优化(存在键字符串化陷阱:1 与 "1" 被视为同一键)
function uniqueByObject(arr) {
const seen = Object.create(null); // 空对象,无原型链,避免继承方法
const res = [];
for (const x of arr) {
if (!seen[key]) {
seen[key] = true;
res.push(x);
}
}
return res;
}
// [1, '1', 2, '2'](示例中恰好保留,但键混淆风险高)
console.log(uniqueByObject([1, "1", 2, "2"]));对象数组去重
按特定字段去重(如 id)
核心工具:ES6 Map(键支持任意类型,无强制类型转换)
实现步骤:
- 创建空
Map实例 - 用
filter遍历数组,以对象的目标字段(如item.id)为Map的键 - 若
Map中无该键,则存入Map并保留当前元素
- 创建空
代码示例:
按单字段去重(保序,保留首个出现的项)
javascriptfunction uniqueByKey(arr, key) { const map = new Map(); return arr.filter((item) => { const k = item[key]; if (!map.has(k)) { map.set(k, true); return true; } return false; }); } // 使用示例 const objArr = [ { id: 1, name: "a" }, { id: 1, name: "b" }, { id: 2, name: "c" }, ]; console.log(uniqueByKey(objArr, "id")); // => [{ id:1, name:"a" }, { id:2, name:"c" }]多字段联合去重(如 id + lang)
javascriptfunction uniqueByKeys(arr, keys) { const map = new Map(); return arr.filter((item) => { const keyStr = keys.map((k) => item[k]).join("\u0001"); return !map.has(keyStr) && map.set(keyStr, true); }); } console.log( uniqueByKeys( [ { id: 1, lang: "en" }, { id: 1, lang: "zh" }, { id: 1, lang: "en" }, ], ["id", "lang"] ) ); // => [{ id:1, lang:"en" }, { id:1, lang:"zh" }]优势:时间复杂度
O(n),无类型转换问题,适配多数对象去重场景
全对象深度去重(慎用)
- 原理:通过
JSON.stringify()将对象转为字符串,再用Set对字符串去重 - 缺陷:对象属性顺序会影响 JSON 字符串结果(如
{a:1,b:2}与{b:2,a:1}会被判定为不同) - 适用场景:对象属性顺序固定的特殊场景
示例实现(属性顺序归一化后再序列化):
javascript
// 将对象按键名排序后再序列化(仅处理浅层键;深层可递归)
function stableStringify(obj) {
const keys = Object.keys(obj).sort();
const normalized = {};
for (const k of keys) normalized[k] = obj[k];
return JSON.stringify(normalized);
}
function uniqueDeep(arr) {
const seen = new Set();
const res = [];
for (const item of arr) {
const s = stableStringify(item);
if (!seen.has(s)) {
seen.add(s);
res.push(item);
}
}
return res;
}
// 示例:属性顺序不同但内容相同的对象被视为重复
console.log(
uniqueDeep([
{ a: 1, b: 2 },
{ b: 2, a: 1 }, // 与上面等价
{ a: 1, b: 3 },
])
);注意
注意:若对象存在嵌套结构且需要深度去重,应在 stableStringify 中对值为对象的属性递归处理;但深度比较的成本较高,优先按业务字段(如 id)去重更高效。
常见陷阱与解决方案
常见误区
- 用对象作哈希表:键被强制字符串化,
1与"1"被视为同一键。 - 想用
indexOf去重:对NaN失效;应使用Set或includes。 - 深度去重直接
JSON.stringify:属性顺序不同会被当作不同对象;需先排序键。
| 陷阱场景 | 问题原因 | 解决方案 |
|---|---|---|
NaN 重复保留 | indexOf底层用===判断,而NaN === NaN为 false,导致indexOf(NaN)永远返回-1 | 将indexOf替换为 ES7 的includes(底层对 NaN 做特殊处理) |
| 数字与字符串混淆 | 对象键会强制转为字符串,导致1与"1"被视为同一键 | 优先使用 Map 而非对象做哈希表,或在存储时标记类型(如key = typeof item + item) |
| 对象属性顺序干扰 | JSON.stringify()会按属性定义顺序生成字符串 | 若需全对象去重,先统一对象属性顺序(如按键名排序)再序列化 |
终极版 unique 函数演进
基础版(仅处理基本类型)
javascript
function unique(arr) {
return [...new Set(arr)];
}进阶版(支持对象按字段去重)
javascript
function unique(arr, key) {
if (!key) return [...new Set(arr)];
const map = new Map();
return arr.filter((item) => {
const k = item[key];
if (!map.has(k)) {
map.set(k, true);
return true;
}
return false;
});
}工程版(支持多场景扩展)
通过 options 参数实现灵活配置:
javascript
/**
* 通用去重函数,支持多场景配置
* @param {Array} arr - 待去重数组
* @param {Object} options - 配置项
* @param {string|Array|Function} [options.key] - 去重键(字符串/数组/函数)
* @param {Function} [options.compare] - 自定义比较函数(返回 true 表示相等)
* @returns {Array} - 去重后的数组
*/
function unique(arr, options = {}) {
const { key, compare } = options;
// 1) 自定义比较函数(复杂等价判断),保序
if (typeof compare === "function") {
const result = [];
for (const item of arr) {
if (!result.some((res) => compare(res, item))) result.push(item);
}
return result;
}
// 2) 多字段联合去重:key 为字段数组,保序
if (Array.isArray(key)) {
const map = new Map();
return arr.filter((item) => {
const k = key.map((k) => item[k]).join("#");
return !map.has(k) && map.set(k, true);
});
}
// 3) 通过函数生成键:更灵活,保序
if (typeof key === "function") {
const map = new Map();
return arr.filter((item) => {
const k = key(item);
return !map.has(k) && map.set(k, true);
});
}
// 4) 单字段去重:key 为字符串,保序
if (typeof key === "string") {
const map = new Map();
return arr.filter(
(item) => !map.has(item[key]) && map.set(item[key], true)
);
}
// 5) 基本类型去重(数字/字符串/Symbol/NaN),保序
return [...new Set(arr)];
}面试答题核心要点
- 优先方案:基本类型用
Set(提及其处理NaN的优势),对象数组按字段用Map - 避坑意识:指出
indexOf处理NaN的缺陷、对象键类型转换问题 - 性能对比:双重循环
O(n²)vs 哈希表O(n),说明优化思路 - 工程思维:展示函数的扩展性(支持多字段、自定义比较),体现代码复用能力
小结与后续
- 先明确需求维度(类型、顺序、深度、复杂度),再选方案;避免为“少数特殊场景”牺牲主流程的可读性与性能。
- 业务中落地优先:基本类型用
Set,对象数组按字段用Map;多字段/定制比较抽象为options,集中维护。
