ES6的Set与Map
- 2020 年 3 月 9 日
- 筆記
Set 和 Map 出现之前
在 ES5 中经常用对象来模拟实现 Set 集合与 Map 集合这两种数据结构,但这种做法带来了一些问题:比如利用 if(obj.size)
检查集合中是否存在某个元素的时候,预期行为是只要存在 size
就能通过 if 判断,但如果 size = 0
,那么也无法继续执行,即使此时元素是存在的。另外,对象的键名只能是字符串,非字符串类型的键名也会通过 toString()
方法被转换成字符串,这意味着 obj[5]
与 obj['5']
没有区别,尽管我们本意是想创建两个不同的键;甚至,当键名是对象的时候,不管我们操作的是哪个键名(obj[key1]
或者 obj[key2]
),实际操作的都是 obj['[object Object]']
,这是因为对象会被转换成字符串 '[object Object]'
,这些都是与我们的预期不符合的。因此,ES6 推出了正式的 Set 和 Map 集合。
Set
调用 new Set()
可以创建一个 Set 集合,之后通过 add()
添加元素,size
访问元素数量。
let set = new Set() set.add('one') set.add('two') set.size // 2
与 ES5 中对象模拟实现不同的是,Set 集合会对添加进来的元素调用 Object.is()
检查是否一致,由于 5
与 '5'
是不同的,所以 Set 可以同时存在这两个元素,不会发生类型转换(唯一的例外是 +0
和 -0
,尽管 Object.is(+0,-0)
返回 false
,但是 Set 集合认为这两个是一致的);同理,也可以添加多个独立对象,它们是不会被转换成字符串的。
set.add(5) set.add('5') let key1 = {} let key2 = {} set.add(key1) set.add(key2) set.size // 4
还可以用 has()
检测是否存在某个元素,用 delete()
移除指定元素,用 clear()
清空整个 Set 集合。
另外,创建 Set 集合的时候支持传参,我们可以使用像数组这样的可迭代对象来初始化 Set 集合(这也是将数组转换成 Set 集合的方法):
let set = new Set([1,2,2,3,4]) set.size // 4
注意:Set 集合为了保证集合元素唯一,会对数组进行元素过滤,这一点稍后可以用来进行数组去重。
那么如何访问集合元素呢?由于 Set 集合没有键名,所以不可能像数组那样通过数值型索引值去访问某个元素,要访问 Set 集合的元素,我们需要先将集合转换成数组。这个使用展开运算符 ...
来实现:
let set = new Set() set.add(1) set.add(2) let array = [...set] array // [1,2]
数组转换成集合+集合转换成数组,就可以实现数组去重:
let array = [1,2,2,3,4,5,6,6] let anotherArray = [...new Set(array)] // [1,2,3,4,5,6]
此外,可以用 forEach()
来迭代 Set 集合中的元素,该方法的回调函数接受三个参数:value
,key
以及集合本身。虽然 Set 没有 key
键名,但为了与数组和 Map
的 forEach()
保持一致,依然提供了 key
参数,它的值与 value
是一样的。
Weak Set
上面的 Set 是一个强引用的集合,这指的是,如果集合中存储着对象的引用(set.add(obj)
),那么即使我们已经在集合外面清除了对该对象的引用(obj = null
),集合中的引用也不受影响。为了避免造成内存泄漏,我们需要一种弱引用的集合,也就是 Weak Set。
Weak Set 只存储对象的弱引用,所以如果把集合外面对象的最后一个强引用移除,则 Weak Set 中的引用也不复存在,这样就避免了内存泄露的情况。此外,它还有一些特点:
- 不可以存储原始值,否则报错
- 不可迭代,所以不能使用
forEach()
,clear()
- 不支持
size
属性 - 不暴露诸如
keys()
,values()
等迭代器
Map
相比 Set,Map 存储的是多对键值对,并且键名和键值支持所有的数据类型。
调用 new Map()
可以创建一个 Map 集合,之后通过 map.set(key,value)
添加键值对,map.get(key)
访问指定键名的键值。
let map = new Map() let obj = {} map.set('name','Jack') map.set(obj,'I am object') // 不同于对象,在 Map 中键名可以是对象 map.get('name') // 'Jack' map.get(obj) // 'I am object' map.get('unexisted key') // 访问不存在的键,返回 undefined
Map 同样也有 has(key)
,delete(key)
,clear()
,size
(返回键值对对数)等方法和属性。
另外,可以使用数组来初始化 Map 集合,批量添加元素。由于 Map 中的元素是键值对,所以传入的数组的元素也是数组:
let map = new Map([['name','Jack'],['age',12]]) map.get('name') // 'Jack' map.get('age') // 12 map.size // 2
Map 的 forEach()
方法的回调函数也接受三个参数:value
,key
以及集合本身,这和数组更为接近,只不过数组对应的第二个参数是数值型索引值。
Weak Map
类似的,Map 也有弱引用集合 Weak Map。Weak Map 的键名必须是对象,且保存着对象的弱引用(如果集合外面引用被清除,则集合中的引用也不复存在,且键值对会跟着被移除);键值则不一定是对象,且当键值是对象时,它保存的依然是强引用。也就是说,Weak Map 的弱引用是针对键名来说的。
Weak Map 可以用来跟踪对象的引用,进而确保将来某一刻需要清除的对象的内存一定能够得到释放,不发生潜在的内存泄露。
举例来说,现在有一个 DOM 元素,它接受用户的输入并将输入的信息存储在一个对象中,如果没有使用 Weak Map ,那么维系 DOM 对象 与 输入信息对象 的映射关系时就有可能产生一个新的关于 DOM 对象的强引用,而在之后清除 DOM 对象原先的强引用时,该强引用可能不会被清除,这导致对象内存实际没有得到释放。但是,如果使用了 Weak Map,将 DOM 对象作为键名,输入信息对象作为键值,那么由于 Weak Map 存储的是对象的弱引用,此时就一定能保证 DOM 对象被移除后(且集合外围对象的最后一个强引用被清除),其内存能够得到释放,不会发生内存泄露的问题。
此外,Weak Map 也可以用来存储对象实例的私有变量:
let Person = (function(name,age){ let privateData = new WeakMap() function Person(){ privateData.set(this,{name:name,age:age}) } Person.prototype.getName = function(){ return privateData.get(this).name } Person.prototype.getAge = function(){ return privateData.get(this).age } }())
在上面的这段代码中,存在着一个 Weak Map(privateData)用来维系多个实例与自身私有变量的映射关系。每次创建新实例的时候,都会往 privateData 这个集合中添加新的”映射条目“(privateData.set(this,{name:name,age:age})
,其中,this
指的是实例),键名是实例,键值是存储私有变量的对象。这么一来,当未来某一天删除实例的时候,由于集合外围的实例对象的强引用被移除,Weak Map 存储的又是实例对象的弱引用,所以保证了实弱引用也会被垃圾回收,不存在内存泄漏的问题。简而言之,只要实例被销毁,相关信息也会跟着销毁,这样就保证了信息的私有性。
此外,Weak Map 还有一些特点:
- 不支持
size
属性 - 不可迭代,因此不支持
forEach()
和clear()