🧊 数据的"洁癖"管家:深入解析 JavaScript Set
🤔 为什么我们需要 Set?
在开发中,我们经常遇到这样的场景:
- 数组去重:从后端获取了一堆标签,需要去除重复项。
- 快速查找:判断某个用户 ID 是否已经在黑名单中。
- 集合运算:求两个用户群体的交集、并集或差集。
如果使用传统的 Array,这些操作往往需要嵌套循环或复杂的逻辑,代码冗长且性能低下(尤其是数据量大时)。而 Set 天生就是为了解决"唯一性"和"高效查找"而生的。
通俗比喻:
Array(数组) :像是一个普通的储物箱。你可以往里面扔任何东西,哪怕是一模一样的苹果,它也会照单全收。如果你想找某个苹果,你得把箱子倒出来一个个看。Set(集合) :像是一个带有自动识别功能的智能货架 。
- 唯一性:如果你试图放入一个已经存在的苹果,货架会直接拒绝:"这个已经有了!"
- 高效性:货架内部有索引,问你"有没有红富士?"它能瞬间回答"有"或"没有",不需要逐个翻找。
📂 目录
- [🔍 核心概念与基本用法](#🔍 核心概念与基本用法)
- [🛠️ 常用 API 详解](#🛠️ 常用 API 详解)
- [⚔️ 实战场景:去重与集合运算](#⚔️ 实战场景:去重与集合运算)
- [💡 进阶技巧:WeakSet 与性能分析](#💡 进阶技巧:WeakSet 与性能分析)
- [❌ 常见误区与坑点](#❌ 常见误区与坑点)
- [💡 总结与选型建议](#💡 总结与选型建议)
1. 🔍 核心概念与基本用法
Set 本身是一个构造函数,用来生成 Set 数据结构。
✅ 基本创建
javascript
// 1. 创建一个空的 Set
const s1 = new Set();
// 2. 通过数组初始化(自动去重)
const s2 = new Set([1, 2, 3, 2, 1]);
console.log(s2); // Set(3) { 1, 2, 3 }
// 3. 通过字符串初始化(字符串也是可迭代对象)
const s3 = new Set("hello");
console.log(s3); // Set(4) { 'h', 'e', 'l', 'o' } -> 注意 'l' 只保留了一个
⚠️ 判断相等的规则
Set 内部判断两个值是否相等,使用的是 "Same-value-zero" 算法。
- 它类似于严格相等运算符 (
===)。 - 主要区别 :在
Set中,NaN等于NaN。
javascript
const s = new Set();
s.add(NaN);
s.add(NaN);
console.log(s.size); // 1,因为 NaN === NaN 在 Set 中被视为相同
s.add({});
s.add({});
console.log(s.size); // 3,因为两个空对象引用地址不同,被视为不同元素
2. 🛠️ 常用 API 详解
Set 的实例方法非常直观,主要分为增、删、查、清四类。
| 方法 | 描述 | 返回值 |
|---|---|---|
add(value) |
添加某个值,返回 Set 结构本身(支持链式调用) | Set 对象 |
delete(value) |
删除某个值,返回一个布尔值,表示删除是否成功 | boolean |
has(value) |
判断某个值是否存在于 Set 中 | boolean |
clear() |
清除所有成员,没有返回值 | undefined |
size |
属性,返回成员总数 | number |
💻 代码示例
javascript
const mySet = new Set();
// 添加元素
mySet.add(1).add(2).add(2); // 链式调用,第二个 2 被忽略
console.log(mySet.size); // 2
// 判断存在
console.log(mySet.has(1)); // true
console.log(mySet.has(3)); // false
// 删除元素
mySet.delete(1);
console.log(mySet.has(1)); // false
// 清空
mySet.clear();
console.log(mySet.size); // 0
🔄 遍历方法
Set 默认是可迭代的,可以使用 for...of 循环,也提供了以下遍历方法:
keys(): 返回键名的遍历器(对于 Set,键名即值)values(): 返回键值的遍历器entries(): 返回键值对的遍历器forEach(): 使用回调函数遍历每个成员
javascript
const set = new Set(["red", "green", "blue"]);
for (let item of set) {
console.log(item);
}
// red
// green
// blue
// 转换为数组进行高阶操作
const arr = [...set];
// 或者
const arr2 = Array.from(set);
3. ⚔️ 实战场景:去重与集合运算
这是 Set 最发光发热的地方。
✅ 场景一:数组去重(最经典用法)
以前我们需要用 indexOf 或 filter 配合对象哈希来去重,现在一行代码搞定。
javascript
const arr = [1, 2, 2, 3, 4, 4, 5];
// 方法:Set -> Array
const uniqueArr = [...new Set(arr)];
// 或者
const uniqueArr2 = Array.from(new Set(arr));
console.log(uniqueArr); // [1, 2, 3, 4, 5]
注意 :这种方法只能去除基本类型 的重复值。如果数组中包含对象
{},由于引用地址不同,无法去重。
✅ 场景二:集合运算(交、并、差)
利用 Set 和数组方法,可以轻松实现数学中的集合运算。
javascript
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
// 1. 并集 (Union): a + b
const union = new Set([...a, ...b]);
console.log(union); // Set(4) { 1, 2, 3, 4 }
// 2. 交集 (Intersection): a ∩ b (既在 a 又在 b)
const intersection = new Set([...a].filter((x) => b.has(x)));
console.log(intersection); // Set(2) { 2, 3 }
// 3. 差集 (Difference): a - b (在 a 但不在 b)
const difference = new Set([...a].filter((x) => !b.has(x)));
console.log(difference); // Set(1) { 1 }
✅ 场景三:高性能查找
当数据量很大时,Array.includes() 的时间复杂度是 O(N) ,而 Set.has() 的时间复杂度接近 O(1)。
javascript
const largeArray = Array.from({ length: 100000 }, (_, i) => i);
const largeSet = new Set(largeArray);
console.time("Array Search");
largeArray.includes(99999);
console.timeEnd("Array Search"); // 较慢
console.time("Set Search");
largeSet.has(99999);
console.timeEnd("Set Search"); // 极快
4. 💡 进阶技巧:WeakSet 与性能分析
📉 WeakSet:垃圾回收的好帮手
WeakSet 结构与 Set 类似,也是不重复值的集合。但它有两个显著区别:
- 成员只能是对象,不能是基本类型值。
- 弱引用:其中的对象如果没有其他引用,会被垃圾回收机制自动回收,不会导致内存泄漏。
适用场景:存储 DOM 节点标记、缓存对象引用等临时性数据。
javascript
const ws = new WeakSet();
const obj = {};
ws.add(obj);
console.log(ws.has(obj)); // true
obj = null; // 解除引用
// 此时,ws 中的 obj 可能会被垃圾回收,具体取决于引擎实现
注意 :
WeakSet不可遍历,没有size属性,也没有keys/values/entries方法。
🚀 性能对比总结
| 操作 | Array | Set | 优势方 |
|---|---|---|---|
| 查找元素 | O(N) (线性扫描) | O(1) (哈希表) | Set (数据越大优势越明显) |
| 添加元素 | O(1) (末尾) | O(1) | 平手 |
| 删除元素 | O(N) (需查找+移动) | O(1) | Set |
| 去重 | 需额外逻辑 | 天然支持 | Set |
| 内存占用 | 较低 | 略高 (哈希表开销) | Array |
5. ❌ 常见误区与坑点
1. 引用类型无法自动去重
javascript
const s = new Set();
s.add({ id: 1 });
s.add({ id: 1 });
console.log(s.size); // 2!因为两个对象内存地址不同
解决方案:如果需要基于对象的某个属性去重,需先处理数据(如转为 JSON 字符串或使用 Map)。
2. +0 和 -0
在 Set 中,+0 和 -0 被视为相同的值。
javascript
const s = new Set();
s.add(+0);
s.add(-0);
console.log(s.size); // 1
3. Set 转数组的顺序
Set 保持插入顺序。遍历时,先添加的元素先被遍历。这一点与某些语言中的 HashSet(无序)不同,请务必记住。
6. 💡 总结与选型建议
📝 核心总结
| 特性 | Array | Set |
|---|---|---|
| 有序性 | ✅ 有序 | ✅ 有序 (插入顺序) |
| 唯一性 | ❌ 可重复 | ✅ 唯一 |
| 查找速度 | 慢 (O(N)) | 快 (O(1)) |
| 数据类型 | 任意 | 任意 (WeakSet 仅限对象) |
| 主要用途 | 列表、栈、队列 | 去重、快速查找、集合运算 |
🚀 博主寄语
- 日常开发 :遇到"去重"需求,无脑选
Set。[...new Set(arr)]是最优雅的写法。 - 性能敏感 :如果需要频繁判断"某元素是否存在",请将数据存入
Set而不是Array。 - DOM 操作 :如果需要标记一组 DOM 元素且不担心内存泄漏问题,可以考虑
WeakSet。 - 面试加分项 :提到
Set基于哈希表 实现,所以查找效率高;提到NaN的特殊处理;提到WeakSet的垃圾回收机制。
记住口诀 :
数组去重用 Set,一行代码解千愁。
查找频繁建集合,哈希原理速度快。
对象引用要注意,地址不同算两个。
弱引用里存节点,内存泄漏不用忧。
交集并集差集算,Filter Has 组合秀。
希望这篇文档能帮你彻底掌握 Set 的用法!如果有疑问,欢迎在评论区留言。👇
喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️