一、Set 简介
在 JS
中 Set
对象是 数据
的一个 集合:
Set
对象允许存储任何类型的数据Set
中存储的数据都是唯一的, 同一个值只会被存储一次Set
中每个元素都是有序的, 每个元素的顺序和它插入的顺序保存一致Set
中值的比较也是遵循 零值相等算法 的, 简单来说就是NaN
会被认为是相等的、0
和-0
被认为是相等的; 更多关于JS
中相等性判断可查阅 「我不知道的 JS」 相等性判断 🔥🔥🔥🔥- 同时
Set
内部实现了Symbol.iterator
接口, 其本身又是一个可迭代对象
, 可被for...of
等语法进行迭代, 关于更多迭代器的相关知识可查阅 《迭代器、生成器详解🔥🔥🔥》
二、基本操作
2.1 创建
创建一个 Set
实例, 可直接通过 Set
构造函数进行创建
new Set()
如果我们需要在创建 Set
实例时, 就给定初始值, 可传一数组, 如下代码所示: MoYuanJun
和 18
是要在初始时插入的元素
const set = new Set(['MoYuanJun', 18])
console.log(set) // Set(2) { 'MoYuanJun', 18 }
2.2 增、删、查
- 可通过
Set.prototype.add(value)
方法为Set
实例新增元素, 如果元素已存在则不进行新增操作, 方法返回当前Set
对象
const set = new Set()
set.add('MoYuanJun')
set.add(18)
console.log(set) // Set(2) { 'MoYuanJun', 18 }
由于 Set.prototype.add(value)
方法返回的是当前 Set
对象, 所以实际上它是支持链式调用的, 上面代码可以修改为:
const set = new Set()
set.add('MoYuanJun').add(18)
console.log(set) // Set(2) { 'MoYuanJun', 18 }
- 可通过
Set.prototype.delete(value)
方法删除Set
对象中指定元素, 该方法返回一个布尔值, 表示是否删除成功
const set = new Set(['MoYuanJun', 18])
set.delete(18) // true
- 可通过
Set.prototype.clear()
方法清除Set
对象中所有元素, 该方法没有返回值
const set = new Set(['MoYuanJun', 18])
set.clear() // undefined
- 可通过
Set.prototype.size
属性获取当前Set
对象中元素的个数
const set = new Set(['MoYuanJun', 18])
set.size // 2
- 可通过
Set.prototype.has(value)
方法判断Set
对象中是否存在指定的元素, 该方法返回一个布尔值
const set = new Set(['MoYuanJun', 18])
set.has(18) // true
三、循环
3.1 作为「迭代对象」
由于 Set
定义了 Set.prototype[@@iterator]()
属性, 所以 Set
实例对象是一个 可迭代
对象, 我们可以使用所有可以操作可迭代对象的语法, 包括 for...of
、展开语法、解构语法、Array.from
等等
const set = new Set(['MoYuanJun', 18])
for (const v of set) {
console.log(v) // MoYuanJun 18
}
const arr = [...set] // [ 'MoYuanJun', 18 ]
3.2 Set.prototype.forEach(callbackFn, thisArg)
可通过 Set.prototype.forEach(callbackFn, thisArg)
方法进行循环, 同 Array.prototype.forEach()
该方法支持传 两个参数
:
- 参数
callbackFn
必填, 每次循环将执行该函数, 函数接收3
个参数:
value
(当前迭代的值)key
(当前迭代的key
, 因为Set
中没有键, 这里的key
值等同于value
)Set
(正在迭代的Set
对象)
- 参数
thisArg
选填, 将会被作为callbackFn
函数的this
指向
const set = new Set(['MoYuanJun', 18])
set.forEach(v => {
console.log(v) // MoYuanJun 18
})
3.3 获取「keys」「values」「entries」迭代器
Set
对象中我们可通过原型方法 Set.prototype.keys()
、Set.prototype.values()
、Set.prototype.entries()
分别获取到 Set
对象中 key
、value
以及 [key, value]
的一个集合
需要注意的是这个几个方法返回的是一个迭代器, 同时在 Set
中实际上是没有 key
值的, 所以 keys()
方法实际上是 values()
方法的别名, 它们所获取到的值是一样的
const set = new Set(['MoYuanJun', 18])
const values = set.values() // 返回包含所有 value 的一个迭代器
values.next() // { value: 'MoYuanJun', done: false }
values.next() // { value: 18, done: false }
values.next() // { value: undefined, done: true }
const keys = set.keys() // 返回包含所有 key 的一个迭代器
keys.next() // { value: 'MoYuanJun', done: false }
keys.next() // { value: 18, done: false }
keys.next() // { value: undefined, done: true }
const entries = set.entries() // 返回包含所有 [key, value] 的一个迭代器
entries.next() // { value: [ 'MoYuanJun', 'MoYuanJun' ], done: false }
entries.next() // { value: [ 18, 18 ], done: false }
entries.next() // { value: undefined, done: true }
实际上这几个方法返回的 迭代器
本身又是一个 可迭代对象
, 也就是它能够直接被 for...of
进行循环迭代
const set = new Set(['MoYuanJun', 18])
const values = set.values() // 返回包含所有 value 的一个可迭代迭代器
for (let value of values) {
console.log('value: ', value) // value: MoYuanJun、value: 18
}
const keys = set.keys() // 返回包含所有 key 的一个可迭代迭代器
for (let key of keys) {
console.log('key: ', key) // key: MoYuanJun、key: 18
}
const entries = set.entries() // 返回包含所有 [key, value] 的一个可迭代迭代器
for (let entry of entries) {
console.log('entry: ', entry) // entry: [ 'MoYuanJun', 'MoYuanJun' ]、entry: [ 18, 18 ]
}
关于更多迭代器的相关知识可查阅 《迭代器、生成器详解🔥🔥🔥》
3.4 循环的顺序
Set
对象是能够记住每次插入元素的顺序的, 当我们循环 Set
对象时, 每个元素输出的顺序是和它首次被插入的顺序保持一致的, 同样的 Set.prototype.keys()
、Set.prototype.values()
、Set.prototype.entries()
等方法拿到所有值的顺序也是和插入顺序保持一致
const set = new Set(['MoYuanJun', 18])
set.add('MoYuanJun').add('hz')
const keys = [...set.keys()] // [ 'MoYuanJun', '18', 'hz' ]
for (const item of set) {
console.log(item) // MoYuanJun、18、hz
}
四、扩展知识
4.1 序列化
首先 Set
对象是无法被 JSON.stringify()
直接序列化, 如下代码所示: 使用 JSON.stringify()
对 Set
序列化的结果为 {}
const set = new Set(['MoYuanJun', 18])
JSON.stringify(set) // {}
直接序列化不行, 但是我们可以通过 JSON.stringify()
的 replacer
参数, 实现对 Set
的序列化, 原理很简单就是在序列化过程中, 如果发现数据类型是 Set
类型, 则将其转为数组进行序列化, 同时添加标志位, 方便后面反序列化
const replace = (key, value) => {
if (!(value instanceof Set)) {
return value
}
// 针对 Set 类型数据做处理: 添加 dataType 标志位、将值转换存储
return {
dataType: 'Set',
value: [...value] // 转为数组
}
}
const originalSet = new Set(['MoYuanJun', 18])
const str = JSON.stringify(originalSet, replace)
console.log(str); // {"dataType":"Set","value":["MoYuanJun",18]}
同理, 如果我们想要针对上文 👆🏻 序列化的结果进行反序列的话, 则需要配置 JSON.parse()
的 reviver
参数, 再反序列过程中判断是否存在标志位, 如果有则将数组转为 Set
即可
const replace = (key, value) => {
if (!(value instanceof Set)) {
return value
}
// 针对 Set 类型数据做处理: 添加 dataType 标志位、将值转换存储
return {
dataType: 'Set',
value: [...value] // 转为数组
}
}
const reviver = (key, value) => {
// 针对带有 dataType = Set 标志位的数据进行处理
if (value?.dataType === 'Set') {
return new Set(value.value)
}
return value
}
const originalSet = new Set(['MoYuanJun', 18])
const str = JSON.stringify(originalSet, replace)
const newSet = JSON.parse(str, reviver)
console.log(newSet); // Set(2) { 'MoYuanJun', 18 }
4.2 值相等性比较
我们都知道在 ==
和 ===
运算中, 不同的 NaN
是被视为不同的值
NaN == NaN // false
NaN === NaN // false
NaN === Number('foo') // false
但是呢在 Set
中关于 键的比较
是基于 零值相等 算法, 也就是说 Set
在比较键时不同的 NaN
是被视为同一个值, 同时 0
和 -0
也是认为是同一个键
const set = new Set()
set.add(NaN).add(0).add(NaN).add(-0)
console.log(set) // Set(2) { NaN, 0 }
更多关于 JS 相等性概念可查阅 「我不知道的 JS」 相等性判断 🔥🔥🔥🔥
4.3 「引用类型数据」的存储
Set
中如果我们存储了一个引用类型数据, 实际上存储起来的是引用地址
const obj = { name: 'lh' }
const set = new Set([obj])
// obj 其实是一个引用地址, 通过引用地址查找值, 因为存起来的也是引用地址
set.has(obj) // true
五、与 「Array」「Set」的区别
5.1 和 「Array」 的差异
Array
和 Set
类型, 在数据存储形式上是有点类似的, 它们都是一组数据的集合, 不过它们还是存在一些重要的区别:
Set | Array | |
---|---|---|
意外的键 | 默认不包含任何键、只包含显式插入的键 | 存在原型, 原型链上的键名可能和对象上的设置的键名产生冲突 |
存储方式 | 按索引存储 | 按键值对进行存储(键值相同) |
值的唯一性 | 每个元素都是唯一的 | 可以存在多个相同元素 |
获取值 | 没有提供获取指定值的方法 | 可通过索引获取值 |
Size | 通过 size 属性直接获取 | 通过计算获取 |
序列化和解析 | 没有元素的序列化和解析的支持 | 支持使用 JSON.stringify() 进行序列化、支持使用 JSON.parse() 解析序列化 |
5.2 和 「Map」 的区别
Set
和 Map
本质上还是很相似的, 甚至 Set
的底层就是通过 Map
实现的, 当然你也可以认为 Set
就是键值相等的 Map
Set | Map | |
---|---|---|
初始值 | 包含要插入值的 一维数组 | 包含要插入键值对的 二维数组 |
添加元素接口 | 通过 add(value) 插入数据 | 通过 set(key, value) 插入、修改数据 |
获取值 | 没有获取指定值的方法 | 可通过 get() 方法获取值 |
键 | 按索引存储 | 按键值对进行存储(键值相同) |
六、应用场景
6.1 数组去重
借用 Set
每个元素都是唯一的特性, 我们可以通过它快速的为数组去重
const arr = [1, 1, 2, 3, 3, 4, 5, 2, 4];
const newArr = [...new Set(arr)];
console.log(newArr); // [1, 2, 3, 4, 5]
6.2 集合运算
使用 Set
类型可以轻松地进行集合运算, 如并集、交集、差集等操作
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);
// 并集
const union = new Set([...setA, ...setB]);
console.log(union); // Set(4) { 1, 2, 3, 4 }
// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log(intersection); // Set {2, 3}
// 差集
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log(difference); // Set {1}
6.3 数据处理
利用 Set
数组的特性可以很容易对数据进行处理
- 检查值的否存在,
Set
类型可用于存储值的集合, 然后通过has()
方法快速检查特定值是否存在
const set = new Set(['apple', 'banana', 'orange']);
console.log(set.has('banana')); // true
console.log(set.has('grape')); // false
-
权限控制: 可以使用
Set
类型来存储用户的权限列表, 然后通过Set
类型api
很容易地进行权限的添加、删除和查询 -
事件去重: 可以使用
Set
类型来存储事件监听器, 这样可以避免重复添加同一个事件监听器Set
-
缓存:
Set
可以用作缓存, 例如存储已经计算的结果或已经访问过的URL
等, 由于Set
可以快速查找元素, 因此可以很快地检查某个值是否存在于集合中, 从而避免重复计算或访问
七、总结
- 可以简单视为
Set
是键值对相同的Map
, 它很大部分特性和Map
是保持一致的 Set
对象允许存储任何类型的数据Set
中存储的数据都是唯一的, 同一个值只会被存储一次Set
中每个元素都是有序的, 每个元素的顺序和它插入的顺序保存一致Set
中值的比较也是遵循 零值相等算法的, 简单来说就是NaN
会被认为是相等的、0
和-0
被认为是相等的; 更多关于JS
中相等性判断可查阅 「我不知道的 JS」 相等性判断 🔥🔥🔥🔥- 同时
Set
内部实现了Symbol.iterator
接口, 其本身又是一个可迭代对象
, 可被for...of
等语法进行迭代, 关于更多迭代器的相关知识可查阅 《迭代器、生成器详解🔥🔥🔥》