Skip to content

Map 与 WeakMap

Map

ES6 以前,在 JS 中实现“键/值”式存储可以使用 Object 来方便高效地完成,也就是使用对象属性作为键,再使用属性来引用值。但是 Object 只接受字符串作为键名。

作为 ES6 的新增特性,Map 是一种新的集合类型,它类似于 Object,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串——值”的对应,Map 结构提供了“值——值”的对应,为这门语言带来了真正的键/值存储机制。Map 的大多数特性都可以通过 Object 类型实现,但二者之间还是存在一些细微的差异。

基本用法

使用 new 关键字和 Map 构造函数可以创建一个空映射:

js
const m = new Map()

如果想在创建的同时初始化实例,可以给 Map 构造函数传入一个可迭代对象,需要包含键/值对数组。可迭代对象中的每个键/值对都会按照迭代顺序插入到新映射实例中:

js
const m1 = new Map([
  ['name', 'song'],
  ['age', 30],
  ['sex', 'male'],
])
m1.size // 3
m1.has('name') // true
m1.get('name') // 'song'

上面代码在新建 Map 实例时,就指定了三个键 nameagesex

Map 实例的属性和方法

Map 结构的实例有以下属性:

  • constructor:构造函数,默认就是 Map 函数。
  • size:返回 Map 实例的成员总数。
js
const m = new Map()
m.set('name', 'song')
m.size // 1

Map 实例的方法分为两大类:操作方法(用于操作数据)遍历方法(用于遍历成员)

操作方法:

  • get(key):根据键名,返回键值,如果找不到 key,返回 undefined。
  • set(key, value):根据键名,设置键值。如果键名存在,则更新其对应的值;如果键名不存在,则新增该键。
  • has(key):如果键名存在,返回 true;否则返回 false
  • delete(key):删除某个键,返回 true。如果删除失败,返回 false。
  • clear():清空所有成员,没有返回值。
js
const m = new Map()
m.set('name', 'song')
m.set({ age: 18 }, 18)
m.set('sex', 'male')

m.has('name') // true
m.get('name') // 'song'
m.delete('name')
m.has('name') // false
m.clear()
m.size // 0

遍历方法:

Map 结构提供三个遍历器生成函数和一个遍历方法:

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历 Map 的所有成员。

提示

Map 的遍历顺序就是插入顺序

js
const m = new Map([
  ['F', 'no'],
  ['T', 'yes'],
])

for (let key of m.keys()) {
  console.log(key)
}
// "F"
// "T"

for (let value of m.values()) {
  console.log(value)
}
// "no"
// "yes"

for (let item of m.entries()) {
  console.log(item[0], item[1])
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of m.entries()) {
  console.log(key, value)
}
// "F" "no"
// "T" "yes"

// 等同于使用m.entries()
for (let [key, value] of m) {
  console.log(key, value)
}
// "F" "no"
// "T" "yes"

可以使用扩展运算符(...)快速的将 Map 结构转为数组结构。

js
const m = new Map()
m.set('name', 'song')
m.set('sex', 'male')
[...m.keys()] // ["name", "sex"]
[...m.values()] // ["song", "male"]
[...m.entries()] // [["name", "song"], ["sex", "male"]]
[...m]  // [["name", "song"], ["sex", "male"]]

Map 的 forEach 方法,与数组的 forEach 方法类似,也可以实现遍历。

js
const m = new Map()
m.set('name', 'song')
m.set('sex', 'male')
m.forEach((value, key) => {
  console.log('Key: %s, Value: %s', key, value)
})
// Key: name, Value: song
// Key: sex, Value: male

与其他数据结构的互相转换

js
// Map 转为数组最方便的方法,就是使用扩展运算符(...)。
const m = new Map()
m.set('name', 'song')
m.set('sex', 'male')
[...m] // [["name", "song"], ["sex", "male"]]
js
// 将数组传入 Map 构造函数,就可以转为 Map。
const m = new Map([
  ['name', 'song'],
  ['sex', 'male'],
])
m // Map {"name" => "song", "sex" => "male"}
js
// 如果所有 Map 的键都是字符串,它可以无损地转为对象。
// 如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
function strMapToObj(strMap) {
  let obj = Object.create(null)
  for (let [k, v] of strMap) {
    obj[k] = v
  }
  return obj
}
const m = new Map()
m.set('name', 'song')
m.set('sex', 'male')
strMapToObj(m) // {name: "song", sex: "male"}
js
// 对象转为 Map 可以通过Object.entries()。
let obj = { name: 'song', sex: 'male' }
let m = new Map(Object.entries(obj))
m // Map {"name" => "song", "sex" => "male"}

// 也可以自己实现一个转换函数。
function objToStrMap(obj) {
  let strMap = new Map()
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k])
  }
  return strMap
}
objToStrMap(obj) // Map {"name" => "song", "sex" => "male"}

选择 Object 还是 Map

选择 Object 还是 Map 只是个人偏好问题,影响不大。不过,对于在乎内存和性能的开发者来说,对象和映射之间确实存在显著的差别。

1. 内存占用

Object 和 Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。 不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50%的键/值对

2. 插入性能

向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快一点。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操作,那么显然 Map 的性能更佳

3. 查找速度

与插入不同,从大型 Object 和 Map 中查找键/值对的性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快。在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择 Object 更好一些

4. 删除性能

使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此,出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null。但很多时候,这都是一种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Map 的 delete()操作都比插入和查找更快。如果代码涉及大量删除操作,那么毫无疑问应该选择 Map

WeakMap

WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是,它与 Map 有两个区别:

  1. WeakMap 只接受对象(null 除外)和 Symbol 值作为键名,不接受其他类型的值作为键名。
js
const wm = new WeakMap()
wm.set(1, 1) // 报错
wm.set(null, 1) // 报错
map.set(Symbol(), 1) // 不报错
  1. WeakMap 的键名所指向的对象,不会阻止垃圾回收。

WeakMap 的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。

注意

WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

js
const wm = new WeakMap()
let key = {}
let obj = { foo: 1 }

wm.set(key, obj)
obj = null
wm.get(key) // { foo: 1 }

WeakMap 的语法

WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有 keys()values()entries()方法),也没有 size 属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。二是无法清空,即不支持 clear 方法。因此,WeakMap 只有四个方法可用:get()set()has()delete()

js
const wm = new WeakMap()

// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined