本文介绍 Set、WeakSet、Map、WeakMap 的基本用法
一、Set
1.1、基本用法
ES6 提供了 新的数据结构 —— Set。它类似于数组,但是成员的值都是唯一的,没有重复。Set 本身是一个构造函数,用来生成 Set 数据结构
const s = new Set();
[2, 3, 5, 5, 6, 4, 4, 8].forEach(x => s.add(x))
for (let i of s) {
console.log(i)
}
// 235648
上面 通过 add 方法向 Set 结构加入成员,结果表明 Set结构不会添加重复的值
Set 函数可以接受一个数组(或者具有 iterator 接口的其他数据结构)作为参数,用来初始化。
const set = new Set([1, 2, 3, 4, 4])
[...set]
// [1, 2, 3, 4]
function divs() {
return [...document.querySelectorAll('div')]
}
const set = new Set(divs())
// 元素去重
set.size
数组去重
[...new Set(array)]
1.2、Set实例的属性和方法
Set 结构的实例有以下属性。
- Set.prototype.constructor:构造函数,默认就是Set函数
- Set.prototype.size:返回 Set 实例的成员总数
Set 的操作方法:
- add(value):添加某个值,返回Set结构本身
- delete(value):删除某个值,返回一个布尔值,表示是否删除成功
- has(value):返回一个布尔值,表示参数是否为 Set 的成员
- clear():清除所有成员,没有返回值
Array .form 方法可以将 Set 结构转为数组
const items = new Set([1, 2, 3, 4, 4])
const array = Array.from(items)
array // [1, 2, 3, 4]
这就提供了一种去除数组的重复元素的方法
function dedupe(array) {
return Array.from(new Set(array))
}
dedupe([1, 1, 2, 3]) // [1, 2, 3]
1.3、遍历操作
Set 结构的实例有 4个遍历方法,可用于遍历成员
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每个成员
Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set保存一个回调函数列表,调用时就能保住按照添加顺序调用。
keys()、values()、entries()
keys 方法、values 方法、entries 方法都返回的是遍历器对象。由于Set 结构没有键名,只有键值(键名 和 键值 是同一个值),所以 keys 方法 和 values 方法的行为完全一致。
let 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)
}
// ['red', 'red']
// ['green', 'green']
// ['blue', 'blue']
entries 方法返回的遍历器同时包括键名 和 键值,所以每次输出一个数组,其两个成员完全相等。
Set 结构的实例默认可遍历,其默认遍历器生成函数就是它的 values方法
Set.prototype[Symbol.iterator] === Set.prototype.values
这意味着,可以省略 values 方法,直接用 for...of 循环遍历 Set
forEach()
Set 结构实例的 forEach 方法用于对每个成员执行某种操作,没有返回值
let set = new Set([1, 2, 3])
set.forEach((value, key) => console.log(value * 2))
// 2
// 4
// 6
forEach 还有第二个参数,表示绑定的 this 对象
遍历的应用
扩展运算符(...) 内部使用 for...of 循环,所以也可以用于 Set 结构
let set = new Set(['red', 'green', 'blue'])
let arr = [...set]
// ['red', 'green', 'blue']
数组的 map 和 filter 方法也可以用于 set
let set = new Set([1, 2, 3,])
set = new Set([...set].map(x => x * 2))
// Set(3) {2, 4, 6}
let set = new Set([1, 2, 3, 4, 5])
set = new Set([...set].filter(x =>( x % 2 ) == 0))
// Set(2) {2, 4}
因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect) 和 差集(Difference)
let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])
// 并集
let union = new Set([...a, ...b])
//Set(4) {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x)))
// Set(2) {2, 3}
// 差集
let difference = new Set([...a].filter(x => !b.has(x)))
// Set(1) {1}
如果想在遍历操作中改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种时利用原Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构:另一种是利用 Array.from 方法。
// 方法一
let set = new Set([1, 2, 3])
set = new Set([...set].map(val => val * 2))
// {2, 4, 6}
// 方法二
let set = new Set([1, 2, 3])
set = new Set(Array.from(set, val => val * 2))
// {2, 4, 6}
二、WeakSet
2.1、含义
WeakSet 结构与 Set 类似,也是步重复的值的集合。但是,它与 Set 有两个区别:
- WeakSet 的成员只能是对象,而不能是其他类型的值。
- WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用。
(也就是说,如果其他对象都不在引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象是否还存在与 WeakSet 之中)
2.2、语法
WeakSet 是一个构造函数,可以使用 new 命令创建 WeakSet 数据结构。
const ws = new WeakSet()
WeakSet 可以接受一个数组或类似数组的对象作为参数。实际上,任何具有 iterable 接口的对象都可以作为 WeakSet 的参数。
const a = [[1, 2], [3, 4]]
const ws = new WeakSet(a)
// WeakSet {[1, 2], [3, 4]}
成为 WeakSet 的成员的是 a 数组的成员,而不是 a 数组本身。这意味着,数组的成员只能是对象。
WeakSet 结构有以下 3 个方法
- WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
- WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员
- WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例中。
const ws = new WeakSet()
const obj = {}
const foo = {}
ws.add(window)
ws.add(obj)
ws.has(window) // true
ws.has(foo) // false
ws.delete(window)
ws.has(window)// false
WeakSet 没有 size 属性,没有办法遍历成员
WeakSet 不能遍历,因为成员都是弱引用,随时可能消失,遍历机制无法保证成员存在,很可能刚刚遍历结束,成员就取不到了。WeakSet 的一个用处是存储 DOM 节点,而不用担心这些节点从文档移除时会引发内存泄漏。
const foos = new WeakSet()
class Foo {
constructor() {
foos.add(this)
}
method() {
if (!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在 Foo 实例上调用')
}
}
}
上面的代码保证了 Foo 的实例方法只能在 Foo 的实例上调用。这里使用 WeakSet的好处是,数组 foos 对实例的引用不会被计入回收机制,所以删除实例的时候不用考虑 foos,也不会出现内存泄漏。
三、Map
3.1、含义和基本用法
JavaScript 的对象(Object) 本质上是键值对的集合(Hash 结构),但是只能用字符串作为键。这给它的使用带来了很大的限制
。为了解决这个问题,ES6提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象) 都可以作为键。(也就是说,Object 结构提供了 “字符串——值” 的对应,Map 结构提供了“值——值”的对应,是一种更完善的 Hash 结构实现)
const m = new Map()
const o = {p: 'Hello World'}
m.set(o, 'content')
m.get(o) // 'content'
m.has(o) // true
m.delete(0) // true
m.has(0) // false
Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组
const map = new Map([
['name', '张三'],
['title', 'Author']
])
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
Map 构造函数姐搜数座作为参数,实际上执行的是下面的算法
const items = [
['name', '张三'],
['title', 'Author']
]
const map = new Map()
items.forEach(
([key, value]) => map.set(key, value)
)
事实上,任何具有 Iterator 接口且每个成员都是一个双元素数组的数据结构都可以当作 Map 构造函数的参数。也就是说,Set 和 Map 都可以用来生成新的 Map。
const set = new Set([
['foo', 1],
['bar', 2]
])
const m1 = new Map(set)
m1. get('foo') // 1
const m2 = new Map([['baz', 3]])
const m3 = new Map(m2)
m3.get('baz') // 3
注意事项
- 如果对同一个键多次赋值,后面的值将覆盖前面的值
- 如果读取一个未知的键,则返回 undefined
- Map 的键实际上是和内存地址绑定的,只要内存地址不一样,就视为两个键。
只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心
const map = new Map()
map.set(['a'], 555)
map.get(['a']) // undefined
- 同理,同样的值的两个实例在 Map 结构中被视为两个键
- 如果 Map 的键是一个简单类型的值,则只要两只严格相等,Map就将其视为一个键,包括 0 -0。另外,虽然 NaN 不严格等于自身,但是 Map 将其视为同一个键。
3.2、实例的属性和操作方法
Map 结构的实例有以下几个属性和操作方法
- size:返回Map 结构的成员总数
- set(key, value):设置 key 所对应的键值,然后返回整个Map 结构。如果 key 已经有值,则键值会被更新,否则就生成该键。set方法返回的是当前的 Map 对象,因此可以采用链式写法。
- get(key):get 方法读取 key 对应的键值,如果找不到 key,则返回 undefined
- has(key):has 方法返回一个布尔值,表示某个键是否在 Map 数据结构中。
- delete(key): delete 方法删除某个键,返回 true。如果删除失败,则返回false
- clear():清除所以成员,没有返回值
3.3、遍历方法
Map 原生提供了 3个 遍历器生成函数和一个遍历方法。
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回所有成员的遍历器
- forEach():遍历 Map 的所有成员
需要特别注意的是,Map 的遍历顺序就是插入顺序。
const map = new Map([
['F', 'no'],
['T', 'yes']
])
for (let key of map.keys()) {
console.log(key)
}
//"F"
// "T"
for (let value of map.values()) {
console.log(value)
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1])
}
// "F" "no"
// "T" "yes"
// 或者
for( let [key, value] of map.entries()) {
console.log(key, value)
}
// "F" "no"
// "T" "yes"
Map 结构的默认遍历器接口(Symbol.interator 属性)就是 entries 方法
map[Symbol.iterator] === map.entries
// true
for(let [key, value] of map) {}
// 等同于
for (let [key, value] of map.entries()) {}
Map 结构转为数组结构的比较快速的方法是结合扩展运算符(...)
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three']
])
[...map.keys()];
// [ 1, 2, 3]
[...map.values()];
// ["one", "two", "three"]
[...map.entries()];
// [[1, "one"], [2, "two"], [3, "three"]]
[...map]
// [[1, "one"], [2, "two"], [3, "three"]]
结合数组的 map 方法、filter 方法,可以实现 Map 的遍历和过滤(Map 本身没有 map 和 filter 方法)
const map0 = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c')
const map1 = new Map(
[...map0].map(([k, v]) => [k * 2, '_' + v])
)
map1
// Map(3) {2 => "_a", 4 => "_b", 6 => "_c"}
const map2 = new Map(
[...map0].filter(([k, v]) => k < 3)
)
// Map(2) {1 => "a", 2 => "b"}
此外,Map 还有一个 forEach 方法,与数组的 forEach 方法类似,也可以实现遍历。forEach 方法还可以接受第二个参数,用于绑定 this
const reporter = {
report: function(key, value) {
console.log("key: %s, Value: %s", key, value)
}
}
map.forEach(function(value, key, map) {
this.report(key, value)
}, reporter)
3.4、与其他数据结构的互相转换
Map 转为数组
使用扩展运算符(...)
const myMap = new Map()
.set(true, 7)
.set(1, 'b');
[...myMap]
数组转为 Map
将数组传入 Map 构造函数就可以转为 Map
new Map([
[true, 7],
])
Map 转为对象
如果 Map 的所有键都是字符串,则可以转为对象
function strMapToObj(strMap) {
let obj = Object.create(null)
for (let [k, v] of strMap) {
obj[k] = v
}
return obj
}
对象转为Map
function objToStrMap(obj) {
let strMap = new Map()
for (let k of Object.kets(obj)) {
strMap.set(k, obj[k])
}
return strMap
}
Map 转为 JSON
Map 转为 JSON 要区分两种情况:
- Map 的建名都是字符串,这是可以选择转为对象JSON
function strMapToJson(strMap) }
return JSON.stringify(strMapToObj(strMap))
}
- Map 的键名有非字符串,这时可以选择转为数组JSON
function mapToArrayJson(map) {
return JSON.stringify([...map])
}
JSON 转为 Map
JSON转为 Map,正常情况下所有键名都是字符串。
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr))
}
有一种特殊情况:整个JSON就是一个数组,且每个数组成员本身又是一个具有两个成员的数组。这是,它可以一一对应地转为Map。这往往时数组转为JSON的逆操作。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr))
}
四、WeakMap
4.1、含义
WeakMap 结构与 Map 结构类似,也用于生成键值对的集合。
- WeakMap 可以使用 set 方法添加成员
- WeakMap 也可以接受一个数组,作为构造函数的参数
WeakMap 和 Map 的区别有以下两点:
- WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名
const map = new WeakMap()
map.set(1, 2)
// TypeError: Invalid value used as weak map key
- WeakMap 的键名所指向的对象不计入垃圾回收机制。
WeakMap 的设计目的在于,又是我们想在某个对象上面存放一些数据,但是这会形成对这个对象的引用
如下例子
const e1 = document.getElementById('foo')
const e2 = document.getElementById('bar')
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素']
]
如上,e1 和 e2 是两个对象,我们通过 arr 数组对这两个对象添加一些文字说明,这就形成了 arr 对 e1 和 e2 的引用。
一旦不再需要这两个对象,我们必须手动删除这个引用,否则垃圾回收机制就不会释放 e1 和 e2 占用的内存。
// 手动删除引用
arr[0] = null
arr[1] = null
上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄漏
WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用,
典型的应用场景是,在网页的 DOM 元素上添加数据时就可以使用 WeakMap 结构。当该DOM 元素被清除,其所对应的 WeakMap 记录就会自动被移除
。
总之,WeakMap的专用场景就是它的键所对应的对象可能会在将来消失的场景,WeakMap 结构有助于防止内存泄漏。
const wm = new WeakMap()
let key = {}
let obj = {foo: 1}
wm.set(key, obj)
obj = null
wm.get(key)
// Object {foo: 1}
上面的代码中,键值 obj 是正常引用的,所以,即使在 WeakMap 外部消除了 obj 的引用,WeakMap 内部的引用依然存在
注意:WeakMap 弱引用的只是键名而不是键值。键值依然是正常引用的。
4.2、WeakMap 的语法
WeakMap 与 Map 在 API 上的区别主要有两个:
- 没有遍历操作(即没有 key()、values()、entries()),也没有 size 属性
(因为没有办法列出所有键名,某个键名是否存在完全不可预测,和垃圾回收机制是否运行相关。)
- 无法清空,即不支持 clear 方法。因此,WeakMap 只有4个方法可用:get()、set()、has()、delete()
4.3、WeakMap 示例
通过 Node 的 process.memoryUsage 方法看出来
首先,打开 Node 命令行。 --expose-gc 参数表示允许手动执行垃圾回收机制
$ node --expose-gc
然后执行下面代码
手动执行一次垃圾回收,保证获取的内存使用状态准确
> global.gc()
undefined
查看内存占用的初始状态,heapUsed 为 4.6MB 左右
> process.memoryUsage()
{ rss: 22802432,
heapTotal: 7585792,
heapUsed: 4720600,
external: 8713 }
> let wm = new WeakMap()
undefined
// 新建一个变量key,指向一个 5 * 1024 * 1024 的数组
> let key = new Array(5 * 1024 * 1024)
undefined
// 设置 WeakMap 实例的键名,也指向 key 数组
// 这时,key 数组的引用计数为 2
// 变量 key 引用一次,WeakMap 的键名引用第二次
> wm.set(key, 1)
WeakMap{}
> global.gc()
undefined
这是内存占用 heapUsed 增加到了(这里应该会增加)
> process.memoryUsage()
{ rss: 22802432,
heapTotal: 7585792,
heapUsed: xxxxxxx,
external: 8713 }
// 然后清除 变量 key 对数组的引用
> key = null
// 再次再次执行垃圾回收
> global.gc()
// 再次查看
> process.memoryUsage()
4.4、WeakMap 用途
1. WeakMap 应用的典型场景就是以 DOM 节点作为键名的场景。
let myElement = document.getElementById('logo')
let myWeakmap = new WeakMap()
myWeakmap.set(myElement, {timesClicked: 0})
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement)
logData.timesClicked++
})
每当发生 click 事件就更新一下状态。一旦这个 DOM 节点删除,改状态就会自动消失,不存在内存泄漏风险。
2. 注册监听事件的 listener 对象很适合用WeakMap 来实现
const listener = new WeakMap()
listener.set(element1, handler1)
listener.set(element2, handler2)
element1.addEventListener('click', listener.get(element1), false)
element2.addEventListener('click', listener.get(element2), false)
监听函数放在 WeakMap 里面。一旦 DOM 对象消失,与它绑定的监听函数也会自动消失。
3. WeakMap 的另一个用处时部署私有属性
const _counter = new WeakMap()
const _action = new WeakMap()
class Countdown {
constructor(counter, action) {
_counter.set(this, counter)
_action.set(this, action)
}
dec() {
let counter = _counter.get(this)
if (counter < 1) return
counter--;
_counter.set(this, counter)
if (counter === 0) {
_action.get(this)()
}
}
}
const c = new Countdown(2, () => console.log('DONE'))
c.dec()
c.dec()
//DONE
上面的代码中,Countdown 类的两个内部属性 —— _counter 和 _action —— 是实例的弱引用,如果删除实例,它们也会随之消失,不会造成内存泄漏