今天在一个需求里边遇到一个使用 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
接口的其他数据结构。
// 直接创建一个Set结构
const s = new Set();
s.add(1);
// Set(1) {1}
// 传入数组的形式创建Set结构
const s2 = new Set([1, 2, 3, 4])
// Set(4) {1, 2, 3, 4}
既然 Set
函数支持传入数组来进行初始化,并且成员的值都是唯一的,没有重复的值,所以可以使用 Set
来实现一个简短的数组去重方法:
// 简单数组去重
const sourceArray = [1, 2, 3, 4, 5, 5, 5, 6, 7, 6, 8]
const newArray = Array.from((new Set(sourceArray)))
console.log(newArray)
// (8) [1, 2, 3, 4, 5, 6, 7, 8]
另外向 Set
添加成员的时候需要注意几个点:
- 添加成员的时候,不会发生类型转换
- 两个 对象 总是不相等的,但是两个
NaN
会被认为是相等的 - 插入顺序就是遍历顺序,所以能保证按照添加顺序调用(不能直接改变结构)
内部使用的算法叫做 “Same-value-zero equality”,类似于精确相等运算符(===)
乍一看可能不明白,我举几个例子🌰
// 插入数字和字符串
let set = new Set()
set.add(1)
set.add('1')
// Set(2) {1, "1"}
// 插入对象
let set = new Set()
let a1 = {}
let a2 = {}
set.add(a1)
set.add(a2)
set.size
// 2
// Set(2) {{…}, {…}}
// 插入 NaN
let set = new Set()
let a1 = NaN
let a2 = NaN
set.add(a1)
set.add(a2)
set.size
// 1
// Set(1) {NaN}
// 顺序保持
let set = new Set()
set.add(1)
set.add(2)
set.add(6)
set.add(4)
set.add(5)
// Set(5) {1, 2, 6, 4, 5}
接着就是 Set
的遍历方法了,
第一类:
keys()
,values()
,entries()
- 返回的都是遍历器对象(Iterator 对象 - SetIterator)
Set
的遍历顺序就是插入顺序Set
结构的实例默认可遍历,它的默认遍历器生成函数就是它的values
方法。
由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以
keys
方法和values
方法的行为完全一致。第二类:
forEach()
const set = new Set(['red', 'green', 'blue']);
// 返回键名
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
// 返回键值
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
// 返回键值对
for (let item of set.entries()) {
console.log(item);
}
// (2)["red", "red"]
// (2)["green", "green"]
// (2)["blue", "blue"]
// 直接遍历
for (let x of set) {
console.log(x);
}
// red
// green
// blue
set.forEach((value, key) => console.log(key + ' : ' + value))
// red : red
// green : green
// blue : blue
2 | Map
数据结构
JavaScript 的对象(
Object
),本质上是 键值对 的集合(Hash
结构),但是传统上只能用 字符串 当作键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6 提供了Map
数据结构。它类似于对象,也是 键值对 的集合,但是 “键” 的范围 不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object
结构提供了 “字符串 — 值” 的对应,Map
结构提供了 “值 — 值” 的对应,是一种更完善的Hash
结构实现。如果你需要 “键值对” 的数据结构,Map
比Object
更合适。
来一段对比的例子:
const data = {};
const key = { key: 1 };
data[key] = 'Value';
console.log(data) // { '[object Object]': 'Value' }
console.log(data[key]) // Value
const key2 = { key: 2 };
data[key2] = 'Key2 Value';
console.log(data[key]) // Key2 Value
console.log(data[key2]) // Key2 Value
console.log(data) // { '[object Object]': 'Key2 Value' }
上面代码原意是将一个 对象key
作为 对象data
的键,但是由于对象只接受字符串作为键名,所以key
被自动转为字符串 [object Object]
。
虽然也能用 data[key]
来调用,但实质是把 key
自动转换成了字符串 [object Object]
来调用,所以当有设置多个对象作为键的时候就会覆盖原先的属性。
下边的代码使用 Map
结构的set
方法,将上面的代码进行复原:
const data = new Map();
const key = { key: 1 };
m.set(key, 'Value')
console.log(m.get(key)) // "Value"
const key2 = { key: 2 };
m.set(key2, 'Key2 Value')
console.log(m.get(key2)) // "Key2 Value"
console.log(data)
// Map { { key: 1 } => 'Value', { key: 2 } => 'Key2 Value' }
可以看到 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
也可以接收传入数组来进行初始化,但是数组内需要是键值对的形式,例如:
// 数组 转为 Map
const data = [
[1, 'one'],
[2, 'two'],
[3, 'three'],
]
const map = new Map(data);
console.log(map)
// Map(3) {1 => "one", 2 => "two", 3 => "three"}
对象不是一个可迭代的结构,可以通过 Object.entries()
转换成可迭代结构
// 对象转为 Map
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
console.log(map)
// Map(2) {"a" => 1, "b" => 2}
Map
也可以转换回其它数据结构
// Map 转为数组
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap] // 使用 扩展运算符 最简便
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
如果所有
Map
的键都是字符串,它可以无损地转为对象。
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名(如本节最开始的Demo所示)
// Map 转为对象
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
Map
转为 JSON
Map
转为 JSON
要区分两种情况。一种情况是,Map
的键名 都是字符串,这时可以选择转为 对象 JSON
。
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
另一种情况是,Map
的键名 有非字符串,这时可以选择转为 数组 JSON
。
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
JSON
转为 Map
这段我直接引用阮一峰老师的文字吧
正常情况下,所有键名都是字符串。
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
但是,有一种特殊情况,整个 JSON
就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。
这时,它可以一一对应地转为 Map
。这往往是 Map
转为数组 JSON
的逆操作。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
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吧
const ws = new WeakSet();
const obj = {}
const arr = []
const arr2 = arr
const arr3 = arr
ws.add(obj)
// WeakSet {{…}}
ws.add(arr)
// WeakSet {Array(0), {…}}
ws.add(1)
// Uncaught TypeError: Invalid value used in weak set
ws.add(NaN)
// Uncaught TypeError: Invalid value used in weak set
ws.add(arr2)
// WeakSet {Array(0), {…}}
ws.has(obj)
// true
ws.has(arr)
// true
ws.has(arr2)
// true
ws.has(arr3)
// true
ws.has({})
// false
ws.has([])
// false
ws.delete(arr)
// true
ws.has(arr)
// false
ws.has(arr2)
// false
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
里边看到,例如以下代码:
let a = { name: 'a' }
const ws = new WeakSet();
ws.add(a)
// WeakSet {{…}}
a = null
// null
console.log(ws)
// WeakSet {{…}}
// - [[Entries]]
// - 0:
// - value: {name: "a"}
// __proto__: WeakSet
一个人坐在沙发上纠结了好久,然后还是去群里问了下,得到了一下回复….我是真的没想到🤣
MeatHill:
虽然weak*
允许被回收,但是大多数情况下,在内存压力不大的时候它不会被回收,所以一般建议默认它不会被回收。
尤其不要在set
之后立刻看,几乎一定不会被回收。GC (垃圾回收机制)的成本不低,不是时时运转的。
Emmmm….. 我裂开🙃
3、WeakMap.prototype.clear()
当前版本或者起草中没有这个方法,这个方法在版本 28(2014 年 10 月 14) 之前是 ECMAScript 6 起草规范的一部分,但是在起草之后的版本中被移除了。它不在是最终标准的一部分了 。