🔥 关于 JS 中 Set 的长篇大论 🔥

一、Set 简介

JSSet 对象是 数据 的一个 集合:

  1. Set 对象允许存储任何类型的数据
  2. Set 中存储的数据都是唯一的, 同一个值只会被存储一次
  3. Set 中每个元素都是有序的, 每个元素的顺序和它插入的顺序保存一致
  4. Set 中值的比较也是遵循 零值相等算法 的, 简单来说就是 NaN 会被认为是相等的、0-0 被认为是相等的; 更多关于 JS 中相等性判断可查阅 「我不知道的 JS」 相等性判断 🔥🔥🔥🔥
  5. 同时 Set 内部实现了 Symbol.iterator 接口, 其本身又是一个 可迭代对象, 可被 for...of 等语法进行迭代, 关于更多迭代器的相关知识可查阅 《迭代器、生成器详解🔥🔥🔥》

二、基本操作

2.1 创建

创建一个 Set 实例, 可直接通过 Set 构造函数进行创建

new Set()

如果我们需要在创建 Set 实例时, 就给定初始值, 可传一数组, 如下代码所示: MoYuanJun18 是要在初始时插入的元素

const set = new Set(['MoYuanJun', 18]) 
console.log(set) // Set(2) { 'MoYuanJun', 18 }

2.2 增、删、查

  1. 可通过 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 }
  1. 可通过 Set.prototype.delete(value) 方法删除 Set 对象中指定元素, 该方法返回一个布尔值, 表示是否删除成功
const set = new Set(['MoYuanJun', 18])

set.delete(18) // true
  1. 可通过 Set.prototype.clear() 方法清除 Set 对象中所有元素, 该方法没有返回值
const set = new Set(['MoYuanJun', 18])

set.clear() // undefined
  1. 可通过 Set.prototype.size 属性获取当前 Set 对象中元素的个数
const set = new Set(['MoYuanJun', 18])

set.size // 2
  1. 可通过 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() 该方法支持传 两个参数:

  1. 参数 callbackFn 必填, 每次循环将执行该函数, 函数接收 3 个参数:
  • value(当前迭代的值)
  • key(当前迭代的 key, 因为 Set 中没有键, 这里的 key 值等同于 value)
  • Set(正在迭代的 Set 对象)
  1. 参数 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 对象中 keyvalue 以及 [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」 的差异

ArraySet 类型, 在数据存储形式上是有点类似的, 它们都是一组数据的集合, 不过它们还是存在一些重要的区别:

Set Array
意外的键 默认不包含任何键、只包含显式插入的键 存在原型, 原型链上的键名可能和对象上的设置的键名产生冲突
存储方式 按索引存储 按键值对进行存储(键值相同)
值的唯一性 每个元素都是唯一的 可以存在多个相同元素
获取值 没有提供获取指定值的方法 可通过索引获取值
Size 通过 size 属性直接获取 通过计算获取
序列化和解析 没有元素的序列化和解析的支持 支持使用 JSON.stringify() 进行序列化、支持使用 JSON.parse() 解析序列化

5.2 和 「Map」 的区别

SetMap 本质上还是很相似的, 甚至 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 数组的特性可以很容易对数据进行处理

  1. 检查值的否存在, Set 类型可用于存储值的集合, 然后通过 has() 方法快速检查特定值是否存在
const set = new Set(['apple', 'banana', 'orange']);
console.log(set.has('banana')); // true
console.log(set.has('grape')); // false
  1. 权限控制: 可以使用 Set 类型来存储用户的权限列表, 然后通过 Set 类型 api 很容易地进行权限的添加、删除和查询

  2. 事件去重: 可以使用 Set 类型来存储事件监听器, 这样可以避免重复添加同一个事件监听器 Set

  3. 缓存: Set 可以用作缓存, 例如存储已经计算的结果或已经访问过的 URL 等, 由于 Set 可以快速查找元素, 因此可以很快地检查某个值是否存在于集合中, 从而避免重复计算或访问

七、总结

  1. 可以简单视为 Set 是键值对相同的 Map, 它很大部分特性和 Map 是保持一致的
  2. Set 对象允许存储任何类型的数据
  3. Set 中存储的数据都是唯一的, 同一个值只会被存储一次
  4. Set 中每个元素都是有序的, 每个元素的顺序和它插入的顺序保存一致
  5. Set 中值的比较也是遵循 零值相等算法的, 简单来说就是 NaN 会被认为是相等的、0-0 被认为是相等的; 更多关于 JS 中相等性判断可查阅 「我不知道的 JS」 相等性判断 🔥🔥🔥🔥
  6. 同时 Set 内部实现了 Symbol.iterator 接口, 其本身又是一个 可迭代对象, 可被 for...of 等语法进行迭代, 关于更多迭代器的相关知识可查阅 《迭代器、生成器详解🔥🔥🔥》

本文正在参加「金石计划」

原文链接:https://juejin.cn/post/7218698736813178938 作者:墨渊君

(0)
上一篇 2023年4月6日 上午10:05
下一篇 2023年4月6日 上午10:15

相关推荐

发表回复

登录后才能评论