在ES5时代,我们用于表示“集合”的数据结构主要是数组(Array)和对象(Object)。ES6标准引入了两个新的数据结构Set和Map,它们也表示“集合”。现在,我们拥有了四种常用的“集合”数据结构。并且,我们可以组合使用它们,来定义更加丰富的数据。
本文将主要介绍Set数据结构的基本用法,并与数组进行对比,希望能帮助大家了解并合理使用Set。
基础篇
一、什么是Set?
Set是一个构造函数,用于生成ES6中新增的数据结构——Set数据结构。这种数据结构与数组类似,但其成员的值都是唯一的,不能重复。
二、如何生成Set数据结构?
和数组一样,Set也是一个对象。我们可以用Set构造函数来生成它。在浏览器控制台中打印的Set结构如下:
const a = new Set([1,2,3]) // Set(3) {1, 2, 3}
const b = new Set(new Map([['juejin','666']])) // Set(1) {Array(2)}
const c = new Set(new Set(['java','C','C++'])) // Set(3) {"java", "C", "C++"}
const d = new Set('456') // Set(3) {"4", "5", "6"}
function test(name){
console.log(new Set(arguments))
}
test('juejin') // Set(1) {"juejin"}
const e = new Set(document.querySelectorAll('div')) // Set(141) {div#custom-bg, ...}
需要注意以下两点:
1、Set结构的成员都是独一无二的,当我们试图添加重复的值时,Set会自动帮我们过滤重复的值。利用这个特性,我们可以非常方便的实现数组去重和数学中的并集、交集、差集。
let a = new Set([1,2,2]) // Set(2) {1, 2}
let b = new Set()
b.add(1).add(2).add(2) // Set(2) {1, 2}
<!--去重:-->
let arr = [1,2,3,3]
[...new Set(arr)] // [1,2,3],任何iterator接口的对象,都可以用扩展运算符转为数组
Array.from(new Set(arr)) // [1,2,3],Array.from可将部署了iterator接口的数据转为数组
<!--并集、交集、差集-->
let set1 = new Set([1,2,3])
let set2 = new Set([4,3,2])
let union = new Set([...set1,...set2]) // Set(4) {1,2,3,4}
let intersect = new Set([...set1].filter(x=>set2.has(x))) // Set(2) {2,3}
let difference = new Set([...set1].filter(x=>!set2.has(x))) // Set(1) {1}
2、Set内部判断两个值是否相等的算法类似于精确相等运算符(===)。区别在于,Set认为两个NaN是相等的,但是精确相等运算符会判断两者不相等。
let a = new Set()
new Set().add(NaN).add(NaN) // Set(1) {NaN}
new Set().add(1).add('1') // Set(2) {1, "1"}
new Set().add({}).add({}) // Set(2) {{…}, {…}}
三、Set的属性和方法
属性:
- Set.prototype.constructor:构造函数,默认Set函数
- Set.prototype.size:返回Set成员总数
4个操作数据的方法:
| 方法 | 用途 | 返回值 |
|---|---|---|
| add(value) | 添加某个值 | Set本身(可链式操作) |
| delete(value) | 删除某个值 | 布尔值,是否删除成功 |
| has(value) | 判断参数是否为Set成员 | 布尔值,是否是Set成员 |
| clear( ) | 清除所有成员 | 无 |
let set = new Set()
set.add(1).add(2)
set.size // 2
set.has(1) // true
set.delete(2) //true
set.has(2) //false
set.clear()
4个遍历方法:
| 方法 | 用途 | 返回值 |
|---|---|---|
| keys(value) | 返回键名的遍历器 | 键名的遍历器 |
| values(value) | 返回键值的遍历器 | 键值的遍历器 |
| entries(value) | 返回键值对的遍历器 | 键值对的遍历器 |
| forEach( ) | 使用回调函数遍历每个成员 | 无 |
注意:因为Set结构本身就带有iterator接口,所以也可以直接用for...of遍历。
let set = new Set([1,2,3])
set.keys() // SetIterator {1, 2, 3}
for(let item of set.keys()){
console.log(item)
}
// 1
// 2
// 3
set.values() // SetIterator {1, 2, 3}
for(let item of set.values()){
console.log(item)
}
// 1
// 2
// 3
set.entries() // SetIterator {1 => 1, 2 => 2, 3 => 3},返回的遍历器同时包含键名和键值。
for(let item of set.entries()){
console.log(item)
}
// 因遍历器同事包含键名键值,所以每次打印出一个数组
// [1,1]
// [2,2]
// [3,3]
set.forEach(item => console.log(value*2))
// 2
// 4
// 6
for(let item of set){
console.log(item)
}
// 1
// 2
// 3
深入篇
通过上面的介绍我们可以发现,Set虽然有去重特性,但在api数量上,远不如数组丰富。例如,若想在遍历中同步修改原来的Set结构,Set没有直接的方法。我们只能先转成数组,处理数据后再新生成一个Set。这就意味着Set很难应付工作中复杂的业务场景。那么,Set与数组相比难道只有去重这个优势吗?接下来,我将从api和代码性能两个维度分析他们。
一、api
1、增
| 方法 | 分析 | |
|---|---|---|
| Set | add | add方法只能从末尾加入元素 |
| Array | push、unshift、splice | 数组可以在任何位置加入一个元素 |
2、删
| 方法 | 分析 | |
|---|---|---|
| Set | delete | delete只能删除某个具体的值,Set没有删除某个位置的值的方法 |
| Array | pop、shift、splice | 数组中可以删除任意位置的元素,但若想删除某个特定的值,则要先找到其位置后删除 |
3、改
| 方法 | 分析 | |
|---|---|---|
| Set | 无 | Set不能修改某个元素 |
| Array | arr[0]=1 |
4、查
| 方法 | 分析 | |
|---|---|---|
| Set | has | 如果含有某个值,返回true,不会返回这个值 |
| Array | indexOf、find、findIndex、includes | 数组的方法更加丰富,用法也更多样化 |
二、性能
关于Set的运行速度,掘金上已经有了一篇很好的文章 《如何使用 Set 来提高代码的性能》,我就不再阐述。文章中两者运行时间的差距可能有些夸张,我用自己和朋友的电脑测量了几次,都没有达到作者所说的那个倍数,不过这些都不重要了。总的来说,在数据量大的时候,Set执行添加、删除、查找操作的速度要明显优于数组,尤其是删除操作,快了很多。
总结
通过以上对Set的分析,我总结下个人对于Set的看法:
1、Set像是一个仓库管理员。它从不关心物品的位置,当物品来了,它就直接扔进去;当某个东西不需要了,它就直接销毁;当你想知道仓库里有没有某个物品,它就立马告诉你答案。即使仓库里存储了大量的货物,它的速度依然很快,当然前提是不能重复!
2、数组像是一个一丝不苟的超市管理员,他能记住超市里每一个物品的位置,他可以对超市里每一个商品进行各种骚操作。但是,当货物越来越多时,他也力不从心,速度越来越慢。
3、如果你需要一个存储数据的地方,而数据的位置和顺序对你来说不重要,你也不会对数据进行其他处理,那么Set将是一个很好的选择。