JS Map与WeakMap对象的介绍与实践

1,755 阅读4分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

Map

Map对象以插入的先后顺序,存储键值对(key-value)形式的数据,任意数据类型都可以作为key或者value。

和Object类似,每个键都必须是唯一的,如果键的数据类型是原始数据类型则进行值比较,对象类型则进行引用比较。

实例属性

  • size 只读属性,返回键值对的个数

方法

  • set(key, value) 添加键值对
  • get(key) 返回与这个键相关联的值
  • clear() 移除所有键值对
  • delete(key) 删除与这个键相等的键值对
  • has(value) 返回是否包含这个值
  • entries() 返回一个包含Map中所有键值对且以插入顺序排列的迭代器,迭代器的每个元素为[key, value]形式的数组
  • forEach(callbackFn, thisArg) 以插入顺序,每一个键值对的key和value分别作为第1和第2个参数执行callback方法
  • keys() 返回一个包含Map中所有键且以插入顺序排列的迭代器
  • values() 返回一个包含Map中所有值且以插入顺序排列的迭代器

Map vs Object

差异概述

  • Map的键可以是任何数据类型,Object的键只能是字符串或者Symbol
  • Map可以通过size获取元素的个数,Object则没有
  • Map本身就是一个迭代器,可以直接进行迭代;Object则需要借助Object对象的静态方法(keysvaluesentries)去进行迭代
  • Map序列化(JSON.stringify)后变成了普通的Object导致丢失本身的数据类型,需要自己去实现,同理,字符串解析成JSON对象也是无法支持Map

插入操作

先放测试代码:

var m = new Map()
var now = performance.now()
for(let i = 0; i<10000; i++) {
    m.set('m'+i, i)
}
console.log(performance.now() - now)

var o = {}
var now = performance.now()
for(let i = 0; i<10000; i++) {
    o['o'+i] = i
}
console.log(performance.now() - now)

当插入个数在1万个,Map和Object的差异并不显著,基本都在3-5ms以内。

到了10万个,Map平均时长在50ms上下,大部分落在45-55ms的区间;Object平均时长60ms上下,大部分落在50-70ms的区间。

到了100万个,Map平均时长在590ms上下,绝大部分落在500-700ms的区间;Object平均时长650ms上下,大部分落在550-750ms的区间,偶尔出现超过850ms的情况。

到了500万个,Map平均时长3542.4ms,绝大部分落在3300-3700ms的区间;Object平均时长3851.2ms,大部分落在3100-4200ms的区间,偶尔出现超过4800ms的情况。

到了800万个,Map平均时长6004.2ms,绝大部分落在5500-6500ms的区间;Object平均时长9171ms,大部分落在8500-9500ms的区间,偶尔出现超过10500ms的情况。

超过1000万个的时候,Map的平均时长7582ms,Object则会非常频繁的导致浏览器假死,对比结束。

插入次数MapObject
10万50ms上下60ms上下
100万590ms上下650ms上下
500万3542.4ms3851.2ms
800万6004.2ms9171ms

结论:在频繁插入操作的情况,Map比Object性能表现更好,且耗时分布更加收敛。

查询操作

var m = new Map()
for(let i = 0; i<1000000; i++) {
    m.set('m'+i, i)
}
var now = performance.now()
for(let j = 0; j<1000000; j++) {
  m.get('m'+j)
}
console.log(performance.now() - now)

var o = {}
for(let i = 0; i<1000000; i++) {
    o['o'+i] = i
}
var now = performance.now()
for(let j = 0; j<1000000; j++) {
  o['o'+j]
}
console.log(performance.now() - now)
查询次数MapObject
10万24.6ms41.9ms
50万52.4ms170.5ms
100万83.2ms354.6ms
500万336.9ms2162.2ms

结论:在频繁查询操作的情况,Map比Object性能表现更好,十万级别的耗时差距在2-4倍,百万级别的耗时差距在4-6倍左右

常见操作

初始化赋值

使用二维数组进行初始化赋值,形如 [[key0, value0], [key1, value1], ...]

var m = new Map([[123, 'abc'], ['aaa', true]])
console.log(m)
// output: {123 => "abc", "aaa" => true}

克隆

从一个Map对象克隆一个新的Map对象,可以从下面的代码看出,对象数据类型的key,即使对象内容变化了,也不会影响查询。同样,对象数据类型的value,也只是克隆了对象的指针。

var o = {name: 'Jack'}
var o2 = {msg: 'hello'}
var m = new Map([['k', o2], ['a', 123]])
m.set(o, [1, 2, 3])
var m2 = new Map(m)
o2.msg = 'hi'
console.log(m)
// output: {"k" => {…}, "a" => 123, {…} => Array(3)}
console.log(m2)
// output: {"k" => {…}, "a" => 123, {…} => Array(3)}
console.log(m === m2)
// output: false
console.log(m.get('k') === m2.get('k'))
// output: true
console.log(m.get('k'))
// output: {msg: 'hi'}
m.delete('k')
m2.set('k', 'v')
console.log(m)
// output: {"a" => 123, {…} => Array(3)}
console.log(m2)
// output: {"k" => "v", "a" => 123, {…} => Array(3)}
o.name = 'David'
console.log(m.get(o))
// output: [1, 2, 3]
console.log(m2.get(o))
// output: [1, 2, 3]

合并

两个Map的合并,在创建的的时候不能直接作为参数传入,需要转换成二维数组,且相同的键,以后面的为准。

var m = new Map([['x', 'abc'], ['y', 233]])
var m2 = new Map([['x', 666], ['z', 'hhh']])
var m3= new Map(m, m2)
var m4 = new Map([...m, ...m2])
console.log(m3)
// output: {"x" => "abc", "y" => 233}
console.log(m4)
// output: {"x" => 666, "y" => 233, "z" => "hhh"}

WeakMap

和Map不同,WeakMap对象所存储的键值对(key-value)中的键,只可以是对象数据类型。

尝试插入非对象数据类型的键时,会触发报错。

Uncaught TypeError: Invalid value used as weak map key
    at WeakMap.set (<anonymous>)
    at <anonymous>:1:3

WeakMap vs Map的差异

两者的差异主要集中在两点:

  • WeakMap的key只能存储对象,而Map无论key还是value,都可以存储任意数据类型
  • WeakMap存储的对象,如果没有其他的引用的话,这个对象将会被垃圾回收。这就是冠以 Weak 的原因,同时也意味着,WeakMap是不可枚举的,也就没有size。

在Chrome的开发者工具Console面板试验以下代码:

方法

相对于Map,WeakMap的弱引用特性导致它的方法只有以下4个:

  • set(key, value) 添加键值对
  • get(key) 返回与这个键相关联的值
  • delete(key) 删除与这个键相等的键值对
  • has(value) 返回是否包含这个值

实用场景

  • 对DOM进行操作并持有DOM节点,使用WeakMap可以使得DOM节点被其他代码逻辑删除了之后,可以方便内存被回收,防止内存泄漏
  • Babel编译ES6下类的私有属性,参考我之前的一篇文章TypeScript小状况之对象的私有字段里面的其中一部分

兼容性

最后,来看看浏览器兼容情况。

Map

Screen Shot 2021-08-23 at 12.40.48 AM.png

WeakMap

Screen Shot 2021-08-23 at 12.40.30 AM.png

基本上,绝大部分的现代浏览器都支持Map和WeakMap,绝大部分使用场景都可以放心使用。