ES6中的 Set 与 Map
虽然数组是js中最常用的一种集合类数据类型,但它存在一些问题,比如说,它自身没有提出重复项的方法。
而说到对象,它有一个缺点是,属性名必须是字符串 或者 symbol 这样的数据类型。但我们有的时候,是需要将对象的键设置为像 数组、对象这样的数据类型。
针对这些问题,ES6 中引入 Set、WeakSet、Map、WeakMap 这四种数据结构。
一、Set
Set 是 ES6 中针对上述问题新增的第一种数据类型,它的组成项中没有重复的项,这样,我们就无需手动去重!
1.1 创建 Set
要新建一个 Set ,可以通过 new Set()
这种实例方法进行创建:
var set = new Set(); |
创建完成后,我们会发现它是类似对象的数据结构,里面没有任何组成项。
当然,你也可以像创建数组一样,使用new Set()
这个方法传入一个数组作为参数,用于实例化 Set 中的项:
var set = new Set([1, 3, 5]); |
通过上面的代码,我们便能看到 Set 中包含了三项。而要读取 Set 中项的个数,则可以使用 size 属性。
前面说过,Set 一个很重要的特点是不包含重复值,如果你像这样创建 Set,可能得不到你预期的结果:
var set = new Set([1, 3, 5, 3, '5', 1, 1]); |
你可能会觉得它能像数组一样,输出6个组成项。但由于 Set 不能有重复值的特性,所以代码中的 1 和 3 这些重复值分别只会保留一个。
不过要注意的是,在 Set 中不存在隐式转换,因此数字 5 和 字符串 ‘5’ 是两个不同的项。
1.2 项的操作
我们知道,数组中存在着操作数组项的各种方法,包括新增(push),删除(pop),清空(arr.length = 0)。而 Set 也不另外,它也有几乎类似的操作。
1.2.1 增加项
你可以通过以下方法来新增 Set 项:
var set = new Set([1]); |
从上面代码中可以看到,该方法返回 Set 本身。对于重复的项,add()
方法会直接忽略添加。
新增的项,不单单只能是基本的数据类型,它也可以为复杂的数据类型,比如数组,对象:
var set = new Set(); |
但是,需要注意的是,因为数组或对象这样复杂的数据类型都是它们自身实例出来的 对象
(new Array()、new Object()) ,所以 {}
和 {}
是两个不同的东西。
另外,对于一些特殊值,在ES5中可能都是不对等的,但在 Set 中,则都被认为是同一个东西:
var set = new Set(); |
1.2.2 删除项
对于删除 Set 中的组成项,我们则可以通过 delete()
方法:
var set = new Set([1, 3, 5]); |
该方法返回一个布尔值,如果删除的项存在,则返回 true,反之则返回 false。
1.2.3 清空项
假如一个 Set 中包含了很多项,我们需要快速清空它包含的项,则需要使用 clear()
方法:
var set = new Set([1, 3, 5]); |
1.2.4 判断包含
Set 提供了 has()
方法,它的作用是判断某个项是否存在于 Set 中,这对于我们查询对应的项无疑是非常方便的:
var set = new Set([1, 3, 5, 'a']); |
该方法返回一个布尔值。
1.3 Set 中的循环操作
读取 Set 中的每一项,最常见的方法是使用 for of
:
for (var key of set) { |
当然,你也可以使用ES5中处理数组的 forEach
方法:
var arr = [1, 3]; |
不过要注意,forEach
对于数组的操作,输出的 key 、val是“键值对”(索引、项的值)。但是 Set 则每次依次都是输出 相同的项。
另外,Set 还提供了其他三个遍历方法,它们分别是:
- keys() : 返回键名的遍历器
- values() : 返回键值的遍历器
- entries() : 返回键值对的遍历器
大体用法如下:
var set = new Set([1, 3]); |
目前为止,Set 中好像没有直接更新项的操作方法。所以,如果你要更新 Set 中的项,必须先把它们转换为数组,再转回 Set 。
1.4 Set 与 数组的互转
Set 和 数组有很多类似的特性,它们各自也有一些不同的方法。我们可以借助它们之间的方法和特性,来完成一些高效的操作。
比如,我们需要翻转 Set 中项的顺序时,可以先把它转换为数组,然后利用数组自身的 reverse()
方法即可:
var set = new Set([1, 3, 5, 'a']); |
又比如,当数组需要去重时,我们可以先将数组转换为 Set,然后再转回 数组。这样就避免了在数组中又循环,又判断。
var arr = [1, 3, 5, 'a', 3, 5, 'a']; |
二、WeakSet
WeakSet 是相对 Set 来说,从字面上理解是 弱的 Set 数据结构。它的用法与 Set 类似,不过也有几个不同的地方。
首先来说说 Set 的问题,假设有如下代码:
var set = new Set(), |
由上面的代码可以看到,虽然我们原始值设置为 null
,以为这样就清除了原始引用,但最后还是打印出了其对应的值,这显然不是我们想要的目的。
类似地,有的时候我们会对一些DOM原始进行事件绑定,绑定完后就在内存中存在着一个引用关系。而如果事后,这些DOM可能会被移除,但引用关系仍然存在,这就造成了内存泄露,这当然不是你想看到的。
为了解决相关问题,ES6 中引入了 WeakSet,它只会以对象的形式进行弱引用,而这种弱引用不会影响垃圾回收机制。
然后是 WeakSet 有着和 Set 相同的方法,比如创建(new WeakSet)、新增项(add)、删除项(delete)、包含项判断(has),但是没有 清空项(clear)的操作:
var weakSet = new WeakSet(), |
需要注意的是,WeakSet 创建函数的参数,只能是对象,而不能传递 基本数据类型,否者会报错:
var arr = [1, 3], |
这里数组中的项是 1、3,它们都是基本数据类型。
你可以把它改为如下形式:
var arr = [[1, 3], {}], |
而对于 WeakSet 中项的操作,新增项函数也不能传递 基本数据类型,但 删除项 和 包含项判断 却没有这个要求:
var arr = [[1, 3], {}], |
另外,WeakSet 还有一些其他与 Set 的不同地方:
- WeakSet 没有
size
属性,所以不能获取其中项的个数 - WeakSet 中的项不能使用
for-of
循环,没有forEach()
方法,所以不能读取到对应的项
总之一句话,要处理一些弱引用关系时,可以尝试着使用 WeakSet 。
三、Map
前面说了很多关于 Set 的内容,但是你会发现,虽然我们可以对 Set 进行各种操作,但是我们无法改变其中的项,换言之,即不能更新 Set 中现有项的内容。
于是,ES6 中又引入了 Map。
3.1 创建 Map
通过实例化方法 new Map()
来创建 Map:
var map = new Map(); |
你也可以把一个数组作为参数传入到 new Map()
中,以下代码的结果同等上面的:
var map = new Map([]); // 传入一个空数组 |
但是,如果传入的数组中的项是非对象,则会抛出错误:
var map = new Map([{}, 3]); |
其实,说到底,Map 中就是一个键值对的集合。
如果 new Map()
传入的参数为数组,且数组项的值仍然为数组,当这个值(假设为 arrItem)的长度只有一项时,则 Map 项只有键,没有值(为 undefined)。当 arrItem 的长度为两项时,则 Map 项既有键,也有值。当 arrItem 的长度多余两项时,则 Map 项也是既有键,也有值,它们的值分别是 arrItem 中的第一项和第二项。有点晕?没关系,用代码来验证下:
var map1 = new Map([[1], [2]]), |
3.2 项的操作
类似 Set,Map 中也有很多关于项操作的方法,比如说,项的新增、项的删除、清空以及判断包含等。
3.2.1 增加项
前面我们知道,Set 是通过 add()
方法来新增项的,而 Map 中的操作则有点差异,它是利用 set(key, value)
的方法新增项的,该方法同时设置了项的值,并且,由于该方法执行后是返回自身,所以我们可以链式调用:
var map = new Map(); |
上面的代码新增了三项,然后通过 size
属性获取了该 Map 的长度。
在过去,对象的键只能是字符串,很容易引发同属性名的键值覆盖的情况。但如今 Map 中可以把对象作为键值,所以,对于值相等的对象,不会导致覆盖,当然你也可以用 symbol :
var map = new Map(), |
3.2.2 读取项
Map 中还提供了 get()
方法,用于读取 Map 中包含的额项,但如果读取项的键不存在,则该方法返回 undefined
:
var map = new Map(); |
3.2.2 删除项
删除项的操作和 Set 相同,该方法返回一个布尔值,若删除的项存在则返回 true,反之则返回 false:
var map = new Map([['name', 'yix'], ['age', 28]]); |
由于前面删除过 age 项,所以最后一行的删除返回 false。
3.2.3 清空项
清空项的操作也和 Set 相同,它对于迅速清空一个包含多项的 Set 尤为方便,该方法没有返回值:
var map = new Map([['name', 'yix'], ['age', 28]]); |
3.2.4 判断包含
判断包含项的操作和 Set 相同,如果对应的项存在,将返回 true,反之则返回 false:
var map = new Map([['name', 'yix'], ['age', 28]]); |
3.3 Map 中的循环操作
要循环操作 Set 中的每一项,我们可以通过 forEach()
方法,以下代码首先读取了 Set 中的每一项,然后对每一项的值加上 ‘$$$’ 三个美元符号:
var map = new Map([['name', 'yix'], ['age', 28]]); |
3.4 Map 与 数组的互转
为了方便数据处理,我们通常会将 Map 和 数组两者相互转化,可以通过以下方式:
var map1 = new Map([['name', 'yix'], ['age', 28]]); |
另外,Map 还提供了其他三个遍历方法,它们分别是:
- keys() : 返回键名的遍历器
- values() : 返回键值的遍历器
- entries() : 返回键值对的遍历器
结合数组转换,我们可以轻松的获取到 Map 中项的键、值:
var map = new Map([['name', 'yix'], ['age', 28]]); |
四、WeakMap
ES6 中引入 WeakMap 的原因和 WeakSet 相同,WeakMap 是相对 Map 来说,它的用法和 Map 类似,但 WeakMap 中的键必须是 对象,并且这种对象的引用是弱引用,它不受垃圾回收机制影响。当引用关系不存在时,则 Map 会移除对应的键值对。
WeakMap 有着和 Map 相同的方法,比如创建(new WeakMap)、新增项(set)、删除项(delete)、包含项判断(has),但是没有 清空项(clear)的操作:
var weakMap = new WeakMap(), |
另外,WeakMap 还有一些其他与 Map 的不同地方:
- WeakMap 没有
size
属性,所以不能获取其中项的个数 - WeakMap 中的项不能使用
forEach()
方法,所以不能读取和操作对应的项
这种弱引用的关系,一般主要用于DOM事件的绑定引用。比如页面有一个按钮 btn,每点击一次,其对应的计数加 1,但当该 btn 元素被移除时,则 btn 元素对应的事件引用也被解除,这样就避免了内存泄漏。也无需手动将 btn 对应的事件设置为 null :
var btn = document.querySelector('.btn'); |