今天在一个需求里边遇到一个使用 MAP 数据结构的场景,就去解了一下 ES6 的新数据结构
ECMAScript 6 内提供了 4种 新结构 Set
, WeakSet
, Map
, WeakMap
,虽然已经是5年前的知识了,但是对于我来说还是新🤣
本来想先聊 Map
的,但是仔细看了阮一峰老师的文章之后发现,确实是要从 Set
开始讲
1 | Set
数据结构
- 类似于数组,但是成员的值都是唯一的,没有重复的值。
- 本身是一个构造函数,用来生成
Set
数据结构。- 拥有两个属性:
Set.prototype.constructor
:构造函数,默认就是Set
函数。Set.prototype.size
:返回Set
实例的成员总数。- 四个操作方法和四个遍历方法:
- 操作方法
Set.prototype.add(value)
:添加某个值,返回 Set 结构本身。Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set的成员。Set.prototype.clear()
:清除所有成员,没有返回值。- 遍历方法
Set.prototype.keys()
:返回 键名 的遍历器Set.prototype.values()
:返回 键值 的遍历器Set.prototype.entries()
:返回 键值对 的遍历器Set.prototype.forEach()
:使用回调函数遍历每个成员
先说 Set
数据结构的创建,因为 Set
本身是一个构造函数,所以可以使用 new
命令来创建,然后通过 add()
来添加成员。
当然也可以通过传入一个数组来进行初始化,或者具有 iterable
接口的其他数据结构。
1 | // 直接创建一个Set结构 |
既然 Set
函数支持传入数组来进行初始化,并且成员的值都是唯一的,没有重复的值,所以可以使用 Set
来实现一个简短的数组去重方法:
1 | // 简单数组去重 |
另外向 Set
添加成员的时候需要注意几个点:
- 添加成员的时候,不会发生类型转换
- 两个 对象 总是不相等的,但是两个
NaN
会被认为是相等的 - 插入顺序就是遍历顺序,所以能保证按照添加顺序调用(不能直接改变结构)
内部使用的算法叫做 “Same-value-zero equality”,类似于精确相等运算符(===)
乍一看可能不明白,我举几个例子🌰
1 | // 插入数字和字符串 |
1 | // 插入对象 |
1 | // 插入 NaN |
1 | // 顺序保持 |
接着就是 Set
的遍历方法了,
第一类:
keys()
,values()
,entries()
- 返回的都是遍历器对象(Iterator 对象 - SetIterator)
Set
的遍历顺序就是插入顺序Set
结构的实例默认可遍历,它的默认遍历器生成函数就是它的values
方法。
由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以
keys
方法和values
方法的行为完全一致。第二类:
forEach()
1 | const set = new Set(['red', 'green', 'blue']); |
2 | Map
数据结构
JavaScript 的对象(
Object
),本质上是 键值对 的集合(Hash
结构),但是传统上只能用 字符串 当作键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6 提供了Map
数据结构。它类似于对象,也是 键值对 的集合,但是 “键” 的范围 不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object
结构提供了 “字符串 — 值” 的对应,Map
结构提供了 “值 — 值” 的对应,是一种更完善的Hash
结构实现。如果你需要 “键值对” 的数据结构,Map
比Object
更合适。
来一段对比的例子:
1 | const data = {}; |
上面代码原意是将一个 对象key
作为 对象data
的键,但是由于对象只接受字符串作为键名,所以key
被自动转为字符串 [object Object]
。
虽然也能用 data[key]
来调用,但实质是把 key
自动转换成了字符串 [object Object]
来调用,所以当有设置多个对象作为键的时候就会覆盖原先的属性。
下边的代码使用 Map
结构的set
方法,将上面的代码进行复原:
1 | const data = new Map(); |
可以看到 Map
数据结构 可以完美实现原意,使用对象作为 键,其它就和对象的结构基本类似。
Map
结构的实例和 Set
结构一样的属性和相似的操作方法
- 拥有两个属性:
Map.prototype.constructor
:构造函数,返回一个函数,它创建了实例的原型。默认是Map
函数。Map.prototype.size
:返回Map
对象的键/值对的数量。- 五个操作方法和四个遍历方法:
- 操作方法
Map.prototype.set(value)
:设置 Map 对象中键的值。返回该 Map 对象。Map.prototype.get(value)
:返回键对应的值,如果不存在,则返回undefined
。Map.prototype.has(value)
:返回一个布尔值,表示 Map 实例是否包含键对应的值。Map.prototype.delete(value)
:删除某个元素,返回一个布尔值,表示删除是否成功。Map.prototype.clear()
:移除 Map 对象的所有键/值对,没有返回值。- 遍历方法
Map.prototype.keys()
:返回 键名 的遍历器Map.prototype.values()
:返回 键值 的遍历器Map.prototype.entries()
:返回 键值对 的遍历器Map.prototype.forEach()
:使用回调函数遍历每个成员
和 Set
一样,Map
也可以接收传入数组来进行初始化,但是数组内需要是键值对的形式,例如:
1 | // 数组 转为 Map |
对象不是一个可迭代的结构,可以通过 Object.entries()
转换成可迭代结构
1 | // 对象转为 Map |
Map
也可以转换回其它数据结构
1 | // Map 转为数组 |
如果所有
Map
的键都是字符串,它可以无损地转为对象。
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名(如本节最开始的Demo所示)
1 | // Map 转为对象 |
Map
转为 JSON
Map
转为 JSON
要区分两种情况。一种情况是,Map
的键名 都是字符串,这时可以选择转为 对象 JSON
。
1 | function strMapToJson(strMap) { |
另一种情况是,Map
的键名 有非字符串,这时可以选择转为 数组 JSON
。
1 | function mapToArrayJson(map) { |
JSON
转为 Map
这段我直接引用阮一峰老师的文字吧
正常情况下,所有键名都是字符串。
1 | function jsonToStrMap(jsonStr) { |
但是,有一种特殊情况,整个 JSON
就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。
这时,它可以一一对应地转为 Map
。这往往是 Map
转为数组 JSON
的逆操作。
1 | function jsonToMap(jsonStr) { |
3 | WeakSet
数据结构
WeakSet
结构与Set
类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
WeakSet
的成员只能是 对象,而 不能是其他类型的值。WeakSet
中的对象都是 弱引用(即垃圾回收机制不考虑 WeakSet 对该对象的引用)WeakSet
不能遍历,是因为成员都是弱引用
那直接看说明,很直接明了,第一个区别也很直接,主要就是第二个区别,什么是 垃圾回收机制不考虑 WeakSet 对该对象的引用
垃圾回收机制依赖引用计数,如果一个值的引用次数不为 0,垃圾回收机制就不会释放这块内存。
结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。
而 WeakSet
里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。
因此,WeakSet
适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些 对象 在 外部消失,它在 WeakSet
里面的引用就会自动消失。
由于上面这个特点,
WeakSet
的成员是不适合引用的,因为它会随时消失。
另外,由于WeakSet
内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,
而垃圾回收机制何时运行是不可预测的,因此ES6
规定WeakSet
不可遍历。
总而言之,言而总之,就是一个只能存储 Object
和 Array
的集合,而且内存泄漏得问题现在应该已经解决了吧….
WeakSet
结构有以下三个方法:
WeakSet.prototype.add(value)
:向 WeakSet 实例添加一个新成员。WeakSet.prototype.has(value)
:返回一个布尔值,表示某个值是否在 WeakSet 实例之中。WeakSet.prototype.delete(value)
:清除 WeakSet 实例的指定成员,返回一个布尔值。
我能想到的场景就是判断是否某个对象是否还存在与场景中,🤦♂️其它的应用场景我真的想不到了….
好了,大概看几个演示Demo吧
1 | const ws = new WeakSet(); |
4 | WeakMap
数据结构
WeakMap
结构与Map
结构类似,也是用于生成键值对的集合。
同时也和 WeakSet
一样,只接受 对象 作为键名( null
除外),不接受其他类型的值作为键名。
并且同样是弱引用,WeakMap
的键名所指向的对象,不计入垃圾回收机制。
实例方法 WeakMap
比 WeakSet
多了一个可用 get()
: 返回 key
关联对象, 或者 undefined
(没有 key
关联对象时)。
结尾
再阮一峰老师的文章最后有一段,但是我实际操作的时候触发垃圾回收,所以 Weak*
的弱引用暂时没有 Demo 。
Chrome 浏览器的 Dev Tools 的 Memory 面板,有一个垃圾桶的按钮,可以强制垃圾回收(garbage collect)。这个按钮也能用来观察 WeakMap 里面的引用是否消失。
其它问题
1、Vue 的不能监听到 Map
和 Set
的数据改变:
Vue 的响应式系统不支持
Map
和Set
,也就是说,当Map
与Set
里面的元素变化时Vue追踪不到这些变化,因此无法做出响应。
2、为啥把对象设置为Null
了,WeakSet/WeakMap
内的成员不会消失
我在写笔记的时候一直想写一个 WeakSet 的弱引用 Demo,但是一直没有成功,就算把变量设置为 null
了,还能可以能从 WeakSet
里边看到,例如以下代码:
1 | let a = { name: 'a' } |
一个人坐在沙发上纠结了好久,然后还是去群里问了下,得到了一下回复….我是真的没想到🤣
MeatHill:
虽然weak*
允许被回收,但是大多数情况下,在内存压力不大的时候它不会被回收,所以一般建议默认它不会被回收。
尤其不要在set
之后立刻看,几乎一定不会被回收。GC (垃圾回收机制)的成本不低,不是时时运转的。
Emmmm….. 我裂开🙃
3、WeakMap.prototype.clear()
当前版本或者起草中没有这个方法,这个方法在版本 28(2014 年 10 月 14) 之前是 ECMAScript 6 起草规范的一部分,但是在起草之后的版本中被移除了。它不在是最终标准的一部分了 。