本文章已制作相关视频,点击跳转B站:深入浅出Map、WeakMap、Set、WeakSet
Map
在ES6之前,键值对存储通过一个Object
对象来实现:
var obj = {
key: "val"
}
ES6实现了一个真正的键值对存储:Map
基本API
- 实例化:
new Map()
我们可以通过new
实例化一个Map
对象:
const m = new Map()
当然,如果想要在实例化的时候就去添加数据,那我们可以添加一个可迭代的对象(iterable object),比如一个array对象:
const m = new Map([
[‘k1‘, ‘v1‘],
[‘k2‘, ‘v2‘],
[‘k3‘, ‘v3‘]
])
console.log(m.size) //> 3
const m = new Map({
[Symbol.iterator]: function* () {
yield [‘k1‘, ‘v1‘]
yield [‘k2‘, ‘v2‘]
yield [‘k3‘, ‘v3‘]
}
})
console.log(m.size) //> 3
-
set(k, v)
,添加一对新的键值对,如果键已存在,则用新值覆盖其旧值
const m = new Map()
m.set(‘k1‘, ‘v1‘)
// set()方法返回当前Map对象,所以可以链式调用
m.set(‘k2‘, ‘v2‘)
.set(‘k3‘, ‘v3‘)
-
get(k)
,根据键得到它对应的值 -
has(k)
,判断是否存在该键
const m = new Map()
m.set(‘k1‘, ‘v1‘)
.set(‘k2‘, ‘v2‘)
.set(‘k3‘, ‘v3‘)
console.log(m.has(‘k1‘)) //> true
console.log(m.get(‘k1‘)) //> v1
console.log(m.get(‘k4‘)) //> undefined
-
size
,得到当前Map
对象的键值对个数 -
delete(k)
:删除指定的键值对 -
clear()
:清空map
对象的所有键值对
遍历
Map
维护了插入顺序,会根据这个顺序进行遍历
const m = new Map()
m.set(‘k1‘, ‘v1‘)
.set(‘k2‘, ‘v2‘)
.set(‘k3‘, ‘v3‘)
// 方式一:
for (const e of m) {
console.log(e)
}
//> [ ‘k1‘, ‘v1‘ ]
//> [ ‘k2‘, ‘v2‘ ]
//> [ ‘k3‘, ‘v3‘ ]
// 方式二:
for (const e of m.entries()) {
console.log(e)
}
//> [ ‘k1‘, ‘v1‘ ]
//> [ ‘k2‘, ‘v2‘ ]
//> [ ‘k3‘, ‘v3‘ ]
// 方式三:
for (const e of m[Symbol.iterator]()) {
console.log(e)
}
//> [ ‘k1‘, ‘v1‘ ]
//> [ ‘k2‘, ‘v2‘ ]
//> [ ‘k3‘, ‘v3‘ ]
// 实际上方式二和方式三调用同一个方法
console.log(m.entries === m[Symbol.iterator])
//> true
// 方式四:
m.forEach((v, k) => {
console.log([k, v])
})
//> [ ‘k1‘, ‘v1‘ ]
//> [ ‘k2‘, ‘v2‘ ]
//> [ ‘k3‘, ‘v3‘ ]
// 方式五:只遍历键
for (const k of m.keys()) {
console.log(k)
}
//> k1
//> k2
//> k3
// 方式六:只遍历值
for (const v of m.values()) {
console.log(v)
}
//> v1
//> v2
//> v3
选择Map还是Object
Map和Object都是用于键值对存储,我们需要了解它们之间的区别,方便在开发的时候进行选择
- 内存开销:在引擎层面上,
Map
和Object
的实现区别很大,一般来说,键值对存储的内存开销是随着键值对数量线性增长的。不过对于大部分浏览器来说,Map
在内存开销上还是略胜一筹,粗略计算,Map
比Object
减少50%的内存开销,也就是说,同样的内存,Map
可以存储更多键值对。 - 插入性能:对于
Map
和Object
,一个插入操作不会被键值对数量所影响,但Map
的速度会稍微快于Object
,如果你的代码有大量插入操作,建议选择Map
。 - 查询性能:某些情况下,浏览器会优化
Object
的存放位置(比如,有连续的整数属性),而在Map中这是不可能的,因此,如果你有大量查询,建议使用Object
。 - 删除性能:删除操作是一件很可怕的事情,如果有删除需要,建议使用伪删除(pseudo-deleting)——将该属性赋值为
undefined
或者null
。如果真的有删除需要,Map
的删除性能会更快一些。
WeakMap
基本API
- 实例化
WeakMap
对象的key只能是Object
实例化的对象或者派生类的对象,如果不是的话,则会报错;WeakMap
对象的value可以是任意类型:
// 1. Object实例化的对象
const k1 = new Object()
// 2. 使用字面量对象,字面量对象实际上也是Object实例化的对象
const k2 = {}
// 3. Object派生类的对象
class Other extends Object{}
const k3 = new Other()
// WeakMap的value可以是任意的基本类型或者引用对象
const wm = new WeakMap([
[k1, 10086],
[k2, "I am China Moblie"],
[k3, new Array("Interesting")]
])
// 如果key不符合规范则会报错
const badWM = new WeakMap([
[1, 2],
["Yoo", "Ugh"]
])
//> TypeError: Invalid value used as weak map key
WeakMap
的其他API基本和Map
对象是一样的,但注意,并没有size
属性和clear()
方法
-
set(k, v)
,添加一对新的键值对,如果键已存在,则用新值覆盖其旧值 -
get(k)
,根据键得到它对应的值 -
has(k)
,判断是否存在该键 -
delete(k)
:删除指定的键值对
弱键
WeakMap
的key只能是Object实例化对象或者派生类对象的目的是,让这个key被弱持有。
假设我们有一个场景,我们需要存储DOM节点的属性以及它的值,我们可能会用到Object
或者Map
,假设使用Map
:
const m = new Map()
// 假设我们需要保存一个登录按钮的属性值
const loginButton = document.querySelector("#login")
m.set(loginButton, {disabled: true})
这样会产生一个问题:当用户登录之后,跑到另外一个页面,登录按钮被移除了,正常来说,这个登录DOM节点应该也应该被垃圾回收器清除,但它被loginButton
变量引用,而loginButton
作为key被map
引用,所以登录DOM节点会保留在内存中,白白占用空间。
这时候解决方法是手动解除引用,要么使用delete
方法删除该键值对,要么等Map
对象被销毁。
如果我们使用WeakMap
对象进行同样的储存:
const wm = new WeakMap()
const loginButton = document.querySelector("#login")
wm.set(loginButton, {disabled: true})
作为WeakMap
对象key的loginButton
不会被算成正式的引用(formal reference),也就是说loginButton
变量相当于不会被wm
引用,这时垃圾回收器就可以把这个loginButton
变量和登录DOM节点都给干掉,释放内存空间。
这样就起到了自动清理的效果,这也是WeakMap
弱持有的目的所在。
不可迭代键
WeakMap
的key是不算正式引用,随时可能会被回收清除掉,因此WeakMap
不提供迭代的功能。
对于
size
属性和clear()
方法,由于它们需要先迭代遍历所有的key才能计算得到,所以同样无法使用。
Set
ECAMScript6引入了Set
类型,它同我们高中学到的集合概念是一直的——确定性、互异性、无序性。
可能
Set
和Map
的set()
方法有点混,Set
意思是集合,名词;而Map.set()
是设置,动词来着,注意区分。
基本API
- 实例化。同
Map
一样在构造器中添加一个可迭代对象作为初始化数据。
const set1 = new Set()
const set2 = new Set([‘v1‘, ‘v2‘, ‘v3‘])
console.log(set2.size)
//> 3
const set3 = new Set(
{
[Symbol.iterator]: function* () {
yield ‘v1‘
yield ‘v2‘
yield ‘v3‘
}
}
)
console.log(set3.size)
//> 3
-
add()
,添加数据const s = new Set() // add()返回当前Set对象,因此可以链式调用 s.add(‘h‘).add(‘e‘).add(‘l‘).add(‘l‘).add(‘o‘)
-
has(v)
,判断是否存在该value
-
delete(v)
,删除该value
-
clear()
,删除Set对象中的所有value
-
size
,返回当前Set
中value
的个数
遍历
和Map
一样,Set
也维护了插入顺序,会根据这个顺序进行遍历。
const s = new Set()
s.add(‘h‘).add(‘e‘).add(‘l‘).add(‘l‘).add(‘o‘)
// 方式一:
for (const v of s) {
console.log(v)
}
//> h
//> e
//> l //! 注意这里只有一个l,这是由于集合的互异性,每个元素都是不一样的
//> o
// 方式二:
for (const v of s.values()) {
console.log(v)
}
// 或者
for (const v of s.keys()) {
console.log(v)
}
//> h
//> e
//> l
//> o
// 方式三:
for (const v of s[Symbol.iterator]()) {
console.log(v)
}
//> h
//> e
//> l
//> o
//! 以上三种方式其实都调用同一个方法
console.log(s.values === s[Symbol.iterator])
console.log(s.keys === s[Symbol.iterator])
// 方式四:
for (const pair of s.entries()) {
console.log(pair)
}
//> [ ‘h‘, ‘h‘ ] //! 键和值都是相等的
//> [ ‘e‘, ‘e‘ ]
//> [ ‘l‘, ‘l‘ ]
//> [ ‘o‘, ‘o‘ ]
// 方式五:
s.forEach((val, sameVal) => {
console.log(`[ ‘${val}‘, ‘${sameVal}‘ ]`)
})
//> [ ‘h‘, ‘h‘ ]
//> [ ‘e‘, ‘e‘ ]
//> [ ‘l‘, ‘l‘ ]
//> [ ‘o‘, ‘o‘ ]
WeakSet
WeakSet
和WeakMap
基本相同,存放的value
只能是Object
实例化的对象或者派生类的对象,并且不能迭代, 也没有clear()
方法、size
属性。