JS数组去重:使用Set实现最简写法
详细讲解在JavaScript中使用Set实现数组去重的最简写法、执行流程、底层SameValueZero判定原理以及对象引用的适用边界。
const unique = [...new Set(arr)];。这行代码通过利用 ES6 原生 Set 集合不允许存在重复元素的特性过滤重复项,再借助扩展运算符将集合重新展开为数组,在大多数日常业务中能以极高的执行效率和极低的心智负担完成去重任务。
一、核心写法与执行代码
在日常开发中,如果需要将一个含有重复项的简单数组变为唯一项数组,只需要结合 Set 构造函数与扩展运算符(Spread Operator)即可。
// 示例输入:包含重复数字的数组
const numbers = [1, 2, 2, 3, 4, 4, 5, 1];
// 核心解法:一行代码去重
const uniqueNumbers = [...new Set(numbers)];
示例输出:
console.log(uniqueNumbers);
// [1, 2, 3, 4, 5]
除了扩展运算符之外,ES6 的 Array.from() 方法同样可以将类似集合的可迭代对象(Iterable)转换为常规数组。以下写法在去重逻辑上与扩展运算符完全等价:
const uniqueNumbers = Array.from(new Set(numbers));
两种形式的核心都是利用 Set 存储机制自动去除重复元素,区别仅在于重新构建数组所选用的语法糖。
二、两步法拆解:数据形态的转换过程
理解这行最简写法的关键在于看清数据形态在内存中的两次流转。代码虽然只有一行,但在执行层面完整经历了两个阶段:
- 数组转集合(
new Set(arr)):将原始数组传入Set构造函数。Set在内部依次遍历数组中的每一项,并按元素唯一性规则决定是否存入集合。遇到已经存在的项时会自动忽略,遍历完成即生成一个去重后的Set实例。 - 集合转回数组(
[...set]):由于Set拥有独立的 API(如add、has、size),它并不具备数组的map、filter或按索引取值等方法。为了让后续代码继续无缝使用数组方法,需要通过...展开运算符将Set遍历解构,包裹在中括号[]中生成全新的数组。
不仅语法精炼,这种写法的执行性能也十分出色。Set 内部基于类似哈希表的结构维护键值,添加与查重操作的平均时间复杂度接近 O(1),整体处理 n 个元素的数组时间复杂度为 O(n)。相比以往使用 filter 配合 indexOf 或双重循环(时间复杂度为 O(n²))的方案,在大数据量下有着明显的性能优势。
三、底层机制:Set 到底如何判断“重复”?
许多初学者会好奇:Set 是怎么判定两个值是不是“相同”的?它使用的是普通的 ==、严格相等 === 还是其他规则?
ECMAScript 规范明确规定,Set 内部判断元素是否相等采用的是 SameValueZero(零值相等) 算法。它的行为绝大部分与严格相等(===)一致,但在处理特殊数值时有两个至关重要的区别:
- 对
NaN的处理:在标准 JavaScript 表达式中,NaN === NaN的结果是false。如果使用传统的indexOf去重,往往无法正确识别数组中的多个NaN。但在Set的SameValueZero规则下,NaN与自身被认定为同一个值,因此多个NaN能够被安全地去重为一个。 - 对正负零的处理:在底层内存中存在
+0与-0,但在Set中,+0与-0被视为相等,只保留一个0。
我们通过一段包含各种边缘类型的测试代码来直观验证这一行为:
const mixedPrimitives = [
'apple', 'apple',
42, 42,
NaN, NaN,
+0, -0,
null, null,
undefined, undefined
];
const deduplicated = [...new Set(mixedPrimitives)];
示例输出:
console.log(deduplicated);
// ['apple', 42, NaN, 0, null, undefined]
可以看到,无论是字符串、数值、空值还是 NaN,Set 都能严格保证每种原始值只保留初次出现的那一个。
四、容易混淆的边界:为什么对象数组去重会“失效”?
在实际项目中,最常见且容易让人困惑的问题是:“为什么我用 Set 去重由对象组成的数组,里面的重复项却依然存在?”
例如执行下面的代码:
const users = [
{ id: 1, name: 'Alice' },
{ id: 1, name: 'Alice' }
];
const uniqueUsers = [...new Set(users)];
console.log(uniqueUsers.length); // 结果仍然是 2!
这个现象看似与“Set 能够去重”相违背,但实际上并非 Set 失效,而是 JavaScript 中原始类型与引用类型的存储模型差异所致。
正如上图所示,当我们在代码中写出两个 { id: 1, name: 'Alice' } 字面量时,JavaScript 引擎在堆内存中开辟了两个完全独立的存储空间(例如地址分别为 0x002 和 0x003)。users 数组中保存的只是这两个不同堆地址的引用指针。
Set 的 SameValueZero 算法在比对对象时,比对的是它们的引用地址,而绝不会深入递归检查对象内部的属性键值对。由于 0x002 !== 0x003,Set 认定这是两个完全不同的值,自然全部予以保留。
如果业务需要根据对象的某个唯一标识(如 id)去除重复项,推荐使用 Map 或在 filter 中借助 Set 记录已访问的键:
// 根据对象的 id 属性去重,保留首次出现的项
function dedupeByField(arr, key) {
const seen = new Set();
return arr.filter(item => {
const val = item[key];
if (seen.has(val)) return false;
seen.add(val);
return true;
});
}
五、语法选择:展开运算符与 Array.from()
在把 Set 重新转为数组时,[...new Set(arr)] 和 Array.from(new Set(arr)) 均可胜任。读者可以根据以下具体诉求选择更合适的一种:
- 追求代码简短直观:优先选用
[...new Set(arr)]。展开运算符语义简明,阅读成本低,是日常编码的标准惯例。 - 需要去重并同步进行映射计算:优先选用
Array.from(new Set(arr), mapFn)。Array.from的第二个参数支持传入一个映射函数,能够一边将集合转为数组一边完成数据的加工处理,从而避免了写成[...new Set(arr)].map(mapFn)时多创建一次中间数组的额外开销。
// 场景示例:去重数字的同时计算平方值
const rawNums = [1, 2, 2, 3];
// 使用 Array.from 的第二个参数,单次遍历完成转换与映射
const squared = Array.from(new Set(rawNums), x => x * x);
console.log(squared); // [1, 4, 9]
六、方案选型与总结
针对前端与 Node.js 常见的数组去重需求,我们可以根据数组元素的内容结构建立清晰的决策路径:
总结而言:
- 对于纯原始类型数组(数字、字符串、布尔值及 NaN),直接使用
[...new Set(arr)]是现代 JavaScript 中无可争议的最优解,语法最短且性能达到O(n)。 Set去重依赖 SameValueZero 比较机制,能自动合并多个NaN以及+0与-0,保持元素首次出现的先后顺序。- 对于对象数组,由于
Set仅比较内存引用指针,直接使用解构无法按属性内容去重;此时应结合Map或维护键值Set来完成针对特定属性的过滤。
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19
JS对象数组转字符串为什么推荐用JSON.stringify()
2026-11-05 09:40:31
相关标签:








