深入浅出Map、WeakMap、Set、WeakSet

本文章已制作相关视频,点击跳转B站:深入浅出Map、WeakMap、Set、WeakSet

Map

在ES6之前,键值对存储通过一个Object对象来实现:

var obj = {
    key: "val"
}

ES6实现了一个真正的键值对存储:Map

基本API

  1. 实例化: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
  1. set(k, v),添加一对新的键值对,如果键已存在,则用新值覆盖其旧值
const m = new Map()
m.set(‘k1‘, ‘v1‘)

// set()方法返回当前Map对象,所以可以链式调用
m.set(‘k2‘, ‘v2‘)
    .set(‘k3‘, ‘v3‘)
  1. get(k),根据键得到它对应的值
  2. 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
  1. size,得到当前Map对象的键值对个数
  2. delete(k):删除指定的键值对
  3. 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都是用于键值对存储,我们需要了解它们之间的区别,方便在开发的时候进行选择

  1. 内存开销:在引擎层面上,MapObject的实现区别很大,一般来说,键值对存储的内存开销是随着键值对数量线性增长的。不过对于大部分浏览器来说,Map在内存开销上还是略胜一筹,粗略计算,MapObject减少50%的内存开销,也就是说,同样的内存,Map可以存储更多键值对。
  2. 插入性能:对于MapObject,一个插入操作不会被键值对数量所影响,但Map的速度会稍微快于Object,如果你的代码有大量插入操作,建议选择Map
  3. 查询性能:某些情况下,浏览器会优化Object的存放位置(比如,有连续的整数属性),而在Map中这是不可能的,因此,如果你有大量查询,建议使用Object
  4. 删除性能:删除操作是一件很可怕的事情,如果有删除需要,建议使用伪删除(pseudo-deleting)——将该属性赋值为undefined或者null。如果真的有删除需要,Map的删除性能会更快一些。

WeakMap

基本API

  1. 实例化

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()方法

  1. set(k, v),添加一对新的键值对,如果键已存在,则用新值覆盖其旧值
  2. get(k),根据键得到它对应的值
  3. has(k),判断是否存在该键
  4. 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类型,它同我们高中学到的集合概念是一直的——确定性、互异性、无序性。

可能SetMapset()方法有点混,Set意思是集合,名词;而Map.set()设置,动词来着,注意区分。

基本API

  1. 实例化。同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
  1. add(),添加数据

    const s = new Set()
    // add()返回当前Set对象,因此可以链式调用
    s.add(‘h‘).add(‘e‘).add(‘l‘).add(‘l‘).add(‘o‘)
    
  2. has(v),判断是否存在该value

  3. delete(v),删除该value

  4. clear(),删除Set对象中的所有value

  5. size,返回当前Setvalue的个数

遍历

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

WeakSetWeakMap基本相同,存放的value只能是Object实例化的对象或者派生类的对象,并且不能迭代, 也没有clear()方法、size属性。

深入浅出Map、WeakMap、Set、WeakSet

上一篇:基本数据类型转换和 String类型的转换


下一篇:[ES6深度解析]6:箭头函数