JavaScript中Set,Map和Object的区别
最近在阅读《Web高效编程与实践优化》的计算机基础一节,看到他讲了JS中Set和Map的区别,以及Object的实现,算是解决了往常的一点疑惑吧,这里简单总结下:
Set
Set一般是使用红黑树实现的,红黑树是一种平衡查找二叉树,它的查找时间复杂度为O(logN)。实际上,Chrome V8的Set是用哈希实现的,它是一个哈希Set
假设我们有这样一段代码:
1 | var set = new Set(); |
哈希的一个关键的地方是哈希算法,即对一堆数或者字符串做哈希运算得到它们的随机值
1 | function ComputeIntegerHash(key, seed) { |
把数字进行各种位运算,得到一个比较随机的数,然后对这个数进行散射散射的目的是得到这个数放在数组的哪个index。
1 | var capacity = 64; |
有20个数,容量capacity从16开始增长,每次扩大一倍,到64的时候,可以保证capacity>size*2,因为只有容量是实际存储大小的两倍时,散射结果重复值才能比较低。
现在要查找key=56是否存在这个Set里面,先把56进行哈希,然后散射,按照存放的时候同样的过程
1 | function SetHas(key){ |
上面是哈希存储结构的一个典型实现,但是Chrome的V8的Set/Map 并不是这样实现的,略有不同。
哈希算法是一样的,但是散射的时候用来去掉高位的并不是用 capacity,而是用capacity的一半,叫做buckets的数量,这个去掉高位其实就是通过hash值和容量做一个按位与运算,效果等同于取余。
插入过程
假设我们依次插入9,33,68,57
Set的存储结构分成三部分,第一部分有3个元素,分别表示有效元素个数、被删除的个数、buckets的数量,前两个数相加就表示总的元素个数。
插入9之后,元素个数加1变成1,初始化的buckets数量为2。
第二部分对应buckets,buckets[0]表示第1个bucket所存放的原始数据的index,源码里面叫做entry,9在data这个数组里面 的index为0,所以它在bucket的存放值为0,并且bucket的散射值为0,所以bucket[0]=0。
第三部分是记录key值的空间,9的entry为0,所以它放在了3+buckets.length+entry*2=5的位置,每个key值都有两个元素空 间,第一个存放key值,第二个是keyChain,它的作用下面将提到
插入33之后
插入68,68的bucket值也为1,和33重复了,因为entry=buckets[1]=1,不为 空,说明之前已经存过了,entry为1指向的数组的位置为 3+buckets.length+entry*2=7,也就是说之前的那个数是放在数组7的位 置,所以68的相邻元素存放值keyChain为7,同时bucket[1]变成68的 entry为2
插入57
查找过程
现在要查找33这个数,通过同样的哈希散射,得到33的bucket=1, bucket[1]=3,3指向的index位置为11,但是11放的是57,不是要找的 33,于是查看相邻的元素为9,非空,可继续查找,位置9存放的是 68,同样不等于33,而相邻的index=10指向位置7,而7存放的就是33 了,通过比较key值相等,所以这个Set里面有33这个数
这里有亮点需要注意:
这里的数据总共是4个数,但是需要比较的次数比较多,key值就 比较了3次,key值的相邻keyChain值比较了2次,总共是5次,比直接来 个for循环还要多。所以数据量比较小时,使用哈希存储速度反而更 慢,但是当数据量偏大时优势会比较明显
还有一个问题就是一旦容量不够,就会触发扩容,会把上述所有的hash计算过程重新来一遍。
Map
和Set基本一致,不同的地方是,map多了存储value的地方,
当然它不是直接存放字符串“hello”,而是存放hello的指针地址, 指向实际存放hello的内存位置。
JS Object
这个可以参考这篇博客:https://sunra.top/posts/37711/