一、开篇引入
想象一下,你是一位文学研究者,接到一个有趣又有点棘手的任务:分析一篇几万字的长篇小说,统计里面究竟出现了多少个不重复的单词。这可不是个轻松活儿,如果手动去数,怕是眼睛都要看花,还很容易出错。要是用传统的编程方法,写一堆循环和条件判断,代码也会变得又长又复杂,维护起来头疼不已。
但别担心,JavaScript 中的 Set 对象就像一位超级助手,能轻松解决这个难题。它就像是一个神奇的收纳盒,只允许每个 “物品”(也就是值)出现一次,天生就自带去重属性。有了它,统计不重复单词数量,简直就是小菜一碟。不仅如此,Set 在很多其他场景中也能大显身手,比如处理数组去重、判断元素是否存在等等 。
二、Set 对象初相识
(一)Set 对象是什么
Set 是 ES6 中新增的一种数据结构,简单来说,它就像是一个特殊的 “篮子”,这个篮子里装的东西不会重复。在数学里,集合是由不同元素组成的,Set 就类似这种概念。比如,你喜欢收集邮票,每一张邮票都独一无二 ,你把这些邮票都放在一个集邮册里,这个集邮册就好比是一个 Set,里面不会出现两张一模一样的邮票。在 JavaScript 中,Set 允许你存储任何类型的值,无论是数字、字符串,还是对象引用,它都会自动帮你确保每个值只出现一次。
(二)与其他数据结构的差异
在 JavaScript 的世界里,我们常用的数组(Array)和对象(Object)都有各自的特点,而 Set 和它们相比,有着明显的不同。
先看数组,数组就像是一个有序的 “收纳箱”,它可以存储各种类型的数据,而且允许元素重复。比如[1, 2, 2, 3]这样的数组是完全合法的,要是你想统计数组里元素的个数,直接用length属性就可以。但要是你想检查数组里是否存在某个元素,就得通过indexOf或者includes方法,遍历整个数组来查找,当数组元素很多的时候,这效率可就不高啦。而 Set 呢,它更像是一个 “独特物品陈列柜”,里面的每一个物品(元素)都是独一无二的。你不能直接通过下标来访问 Set 里的元素,因为它没有像数组那样的索引概念。不过,Set 提供了一些方便的方法,像has方法可以快速判断某个元素是否在 Set 中,这可比数组查找元素快多了。而且 Set 还有size属性,用来获取 Set 中元素的数量。
再说说对象,对象是由键值对组成的,就像一个 “有标签的盒子”,每个键就像是标签,对应着一个值。比如{name: '张三', age: 20},通过键name就能找到对应的值张三。对象的键必须是字符串或者 Symbol 类型,要是你用其他类型的值作为键,JavaScript 会自动把它转换成字符串。而 Set 里的元素没有键值对的概念,它只是单纯地存储一个个唯一的值。另外,对象没有直接判断某个值是否存在的方法,你得通过判断某个键是否存在来间接判断对应的值是否存在 ,但 Set 直接用has方法就能搞定。
三、Set 对象的基础用法
(一)创建 Set 实例
在 JavaScript 中,创建 Set 实例非常简单,使用Set构造函数就可以轻松搞定。
如果你想创建一个空的 Set,直接new Set()就可以啦。比如:
let emptySet = new Set();
这种创建方式适用于你还不确定要往 Set 里添加什么元素,先 “搭个空架子” 的情况 。就像你准备打造一个收藏馆,但还没确定要收藏哪些宝贝,就先把场馆建起来。
要是你已经有一些数据,想直接用这些数据创建 Set,那可以把这些数据放在一个数组里,作为Set构造函数的参数。例如:
let numberArray = [1, 2, 3, 3, 4];
let numberSet = new Set(numberArray);
console.log(numberSet); // 输出 Set {1, 2, 3, 4}
这里,数组numberArray里有重复的数字 3,但创建成 Set 后,重复的 3 就被自动过滤掉了。这种方式适合在你有一组数据,并且希望快速去除重复值的时候使用。比如说,你有一份学生名单,里面可能有重复的名字,你想快速得到一份不重复的名单,就可以把这份名单数据放到数组里,然后用这种方式创建 Set 。
(二)常用方法详解
- 添加元素(add):
使用add方法可以向 Set 中添加元素。这个方法会返回 Set 对象本身,所以可以进行链式调用。来看个例子:
let mySet = new Set();
mySet.add(1).add(2).add(3);
console.log(mySet); // 输出 Set {1, 2, 3}
需要注意的是,Set 的特性决定了重复元素不会被添加。比如你再执行mySet.add(2),虽然代码不会报错,但 Set 里元素并不会增加。这就好比你的收藏馆里已经有了一幅达芬奇的《蒙娜丽莎》,别人再送你一幅一模一样的,你也只能把它拒之门外 ,收藏馆里的藏品数量不会改变。
2. 删除元素(delete):
delete方法用于从 Set 中删除指定元素,它会返回一个布尔值,表示删除操作是否成功。看下面的代码:
let mySet = new Set([1, 2, 3]);
console.log(mySet.delete(2)); // 输出 true,说明删除成功
console.log(mySet); // 输出 Set {1, 3}
console.log(mySet.delete(4)); // 输出 false,因为Set中没有4,删除失败
当你需要从 Set 中移除某个不再需要的元素时,就可以使用这个方法,就像从收藏馆里清理掉一件不再有价值的藏品。
3. 检查元素是否存在(has):
has方法用来检查 Set 中是否存在某个元素,它会返回一个布尔值。如果存在则返回true,否则返回false。比如:
let mySet = new Set([1, 2, 3]);
console.log(mySet.has(2)); // 输出 true
console.log(mySet.has(4)); // 输出 false
在编写程序时,常常需要根据某个元素是否存在来执行不同的逻辑,has方法就能派上大用场。比如在一个用户管理系统中,你可以用它来检查某个用户 ID 是否已经存在于已注册用户的 Set 中 。
4. 获取 Set 大小(size):
size是 Set 的一个属性,通过它可以获取 Set 中元素的数量。示例如下:
let mySet = new Set([1, 2, 3, 4, 5]);
console.log(mySet.size); // 输出 5
这就像是统计你的收藏馆里一共有多少件藏品,一目了然。在很多场景下,了解 Set 的大小能帮助我们做出合适的决策,比如在分页展示数据时,根据 Set 的大小来确定总页数 。
5. 清空 Set(clear):
clear方法可以清空 Set 中的所有元素,让 Set 变回初始的空状态。例如:
let mySet = new Set([1, 2, 3]);
mySet.clear();
console.log(mySet); // 输出 Set {},表示空Set
当你需要重置数据,把 Set 里的数据全部清空时,clear方法就很有用。比如在一个游戏的关卡数据管理中,每完成一个关卡,可能就需要清空当前关卡的临时数据 Set,为下一关做准备 。
四、Set 对象的高级玩法
(一)集合运算
- 并集:在数学概念里,并集是由所有属于集合 A 或者属于集合 B 的元素所组成的集合。在 JavaScript 中,实现两个 Set 的并集操作非常简单。假设有两个 Set,setA和setB,可以使用展开运算符(spread operator)来轻松实现并集操作,代码如下:
let setA = new Set([1, 2, 3]);
let setB = new Set([3, 4, 5]);
let unionSet = new Set([...setA, ...setB]);
console.log(unionSet); // 输出 Set {1, 2, 3, 4, 5}
在实际应用中,比如在一个电商系统中,有两个商品分类集合,一个是热门商品集合,另一个是新品集合。当需要展示所有热门商品和新品时,就可以通过并集操作,将这两个集合合并,得到一个包含所有热门商品和新品的新集合,方便统一展示和管理 。
2. 交集:交集是由所有既属于集合 A 又属于集合 B 的元素所组成的集合。通过filter方法和has方法可以实现 Set 的交集运算,代码示例如下:
let setA = new Set([1, 2, 3]);
let setB = new Set([2, 3, 4]);
let intersectionSet = new Set([...setA].filter(x => setB.has(x)));
console.log(intersectionSet); // 输出 Set {2, 3}
在实际场景中,比如在一个社交平台,有两个用户标签集合,一个是喜欢旅游的用户集合,另一个是喜欢摄影的用户集合。通过交集运算,就能找出既喜欢旅游又喜欢摄影的用户,针对这部分用户推送相关的旅游摄影活动,能大大提高活动的参与度 。
3. 差集:差集是由所有属于集合 A 但不属于集合 B 的元素所组成的集合。实现 Set 的差集运算也不难,代码如下:
let setA = new Set([1, 2, 3]);
let setB = new Set([2, 3, 4]);
let differenceSet = new Set([...setA].filter(x =>!setB.has(x)));
console.log(differenceSet); // 输出 Set {1}
差集在对比数据差异方面非常有用。例如在一个版本管理系统中,有上一版本的文件集合和当前版本的文件集合,通过差集运算,可以快速找出当前版本新增或删除的文件,方便进行版本更新和管理 。
(二)与数组的转换
在 JavaScript 开发中,经常需要在 Set 和数组之间进行转换 。
Set 转数组:使用展开运算符...或者Array.from()方法都可以将 Set 转换为数组。展开运算符的方式很简洁,例如:
let mySet = new Set([1, 2, 3]);
let myArray1 = [...mySet];
console.log(myArray1); // 输出 [1, 2, 3]
Array.from()方法同样有效,示例如下:
let mySet = new Set([1, 2, 3]);
let myArray2 = Array.from(mySet);
console.log(myArray2); // 输出 [1, 2, 3]
数组转 Set:通过Set构造函数,传入一个数组,就能将数组转换为 Set。例如:
let myArray = [1, 2, 2, 3];
let mySet = new Set(myArray);
console.log(mySet); // 输出 Set {1, 2, 3}
在实际应用中,利用 Set 去重数组后再转回数组是很常见的操作。比如有一个包含重复元素的用户 ID 数组,在进行后续处理前,需要先去重。可以先将数组转换为 Set 去重,然后再转回数组,如下:
let userIds = [1, 2, 2, 3, 4, 4];
let uniqueUserIds = Array.from(new Set(userIds));
console.log(uniqueUserIds); // 输出 [1, 2, 3, 4]
这样处理后,得到的uniqueUserIds数组就没有重复元素了,方便后续对用户 ID 进行处理,比如查询用户信息等操作 。
五、Set 对象的应用场景
(一)数组去重
在实际开发中,数组去重是一个非常常见的需求。比如,有一个记录用户操作日志的数组,里面可能存在重复的操作记录,我们需要得到一个不重复的操作记录列表 。使用 Set 对象来实现数组去重,代码简洁又高效。示例代码如下:
let numberArray = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
let uniqueArray = Array.from(new Set(numberArray));
console.log(uniqueArray); // 输出 [1, 2, 3, 4]
或者使用展开运算符,代码更加简洁:
let numberArray = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
let uniqueArray = [...new Set(numberArray)];
console.log(uniqueArray); // 输出 [1, 2, 3, 4]
这种方式的时间复杂度为 O (n),比传统的双层循环去重方式(时间复杂度为 O (n^2))要快得多,大大提高了代码的执行效率 。
(二)数据唯一性验证
在很多场景下,我们需要确保数据的唯一性。例如在一个用户注册系统中,每个用户的 ID 必须是唯一的。当新用户注册时,就可以利用 Set 来验证用户 ID 是否已经存在 。示例代码如下:
// 假设已存在的用户ID Set
let existingUserIds = new Set([1001, 1002, 1003]);
// 新注册用户的ID
let newUserId = 1004;
if (existingUserIds.has(newUserId)) {
console.log('该用户ID已存在,请重新输入!');
} else {
existingUserIds.add(newUserId);
console.log('注册成功!');
}
在表单验证中,Set 也能发挥作用。比如一个标签选择表单,用户选择的标签不能重复,就可以通过 Set 来验证用户选择的标签是否有重复,确保数据的有效性 。
(三)其他场景拓展
在缓存管理方面,Set 可以用来记录已经缓存的数据。当需要获取数据时,先检查 Set 中是否存在该数据的标识,如果存在,说明数据已经缓存,可以直接从缓存中获取,提高数据获取的速度 。
在统计网站的不重复访问记录时,Set 同样适用。每次有用户访问网站,将用户的唯一标识(比如 IP 地址或者用户 ID)添加到 Set 中,最后通过 Set 的size属性就能得到不重复的访问次数 。
另外,在一些算法场景中,比如图的遍历算法中,Set 可以用来记录已经访问过的节点,避免重复访问,提高算法效率 。
六、使用 Set 对象的注意事项
(一)值的唯一性细节
在使用 Set 对象时,它判断值唯一性的机制非常关键 。Set 采用 “SameValueZero” 算法来判断两个值是否相等,这和严格相等运算符===类似,但有一个特殊情况,就是NaN。在 JavaScript 中,NaN和任何值都不相等,包括它自身(NaN!== NaN),然而在 Set 中,NaN被视为相等的值,也就是说,Set 中只能存储一个NaN。例如:
let set = new Set();
set.add(NaN);
set.add(NaN);
console.log(set.size); // 输出 1
这个特性在处理一些包含NaN的数据时需要特别注意,比如在统计数据中的唯一值时,如果数据中存在NaN,Set 会自动将多个NaN合并为一个,避免重复计数 。
(二)对象引用问题
Set 存储的是对象的引用,而不是对象本身。这就意味着,当你向 Set 中添加一个对象时,实际上是把这个对象的内存地址存进了 Set。如果后续你在 Set 外部修改了这个对象,Set 中存储的这个对象引用所指向的对象也会发生变化,这可能会导致一些意想不到的结果。例如:
let obj1 = { name: '张三' };
let mySet = new Set();
mySet.add(obj1);
console.log(mySet.has(obj1)); // 输出 true
// 修改obj1
obj1.name = '李四';
console.log(mySet.has(obj1)); // 依然输出 true,虽然对象内容改变,但引用不变
// 创建一个内容相同但引用不同的对象
let obj2 = { name: '李四' };
console.log(mySet.has(obj2)); // 输出 false,因为obj2和obj1引用不同
为了避免这种因对象引用导致的意外情况,在向 Set 中添加对象时,要确保对象的状态不会在 Set 外部被意外修改。如果需要存储不可变的对象,可以考虑使用Object.freeze()方法将对象冻结,使其属性不能被修改,这样就能保证 Set 中对象的稳定性 。
JavaScript 中的 Set 对象就像是一个功能强大的 “独特元素管理器”,它以独特的方式存储数据,有着简洁而实用的用法,在众多场景中都能发挥重要作用。它的唯一性确保了数据的纯净,不会出现重复元素的干扰;丰富的方法,如add、delete、has等,让我们能轻松地对数据进行添加、删除和查询操作 。无论是在数组去重、数据唯一性验证,还是在缓存管理、统计不重复访问记录等场景中,Set 都展现出了极高的应用价值,大大提高了我们的开发效率和代码质量 。