Skip to content

Map 数据结构

Map 是 ES6 新增的键值对集合,与对象不同的是,Map 的键可以是任意类型。

基本用法#

创建 Map#

// 空 Map
const map1 = new Map()
// 从二维数组创建
const map2 = new Map([
['name', '张三'],
['age', 25],
])
console.log(map2) // Map {'name' => '张三', 'age' => 25}
// 从其他 Map 创建
const map3 = new Map(map2)
// 从 Object 创建
const obj = { a: 1, b: 2 }
const map4 = new Map(Object.entries(obj))

设置和获取#

const map = new Map()
// 设置
map.set('name', '张三')
map.set(123, 'number key')
map.set(true, 'boolean key')
map.set({ id: 1 }, 'object key')
map.set(null, 'null key')
map.set(undefined, 'undefined key')
// 链式调用
map.set('a', 1).set('b', 2).set('c', 3)
// 获取
map.get('name') // '张三'
map.get(123) // 'number key'
map.get('notexist') // undefined

对象作为键#

const map = new Map()
const key1 = { id: 1 }
const key2 = { id: 1 }
map.set(key1, 'value1')
map.set(key2, 'value2')
// 不同对象是不同的键
console.log(map.size) // 2
console.log(map.get(key1)) // 'value1'
console.log(map.get(key2)) // 'value2'
console.log(map.get({ id: 1 })) // undefined(新对象,不同引用)

判断和删除#

const map = new Map([['name', '张三']])
// 判断存在
map.has('name') // true
map.has('age') // false
// 删除
map.delete('name') // true
map.delete('notexist') // false
// 清空
map.clear()
console.log(map.size) // 0

遍历方法#

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

const map = new Map([
['name', '张三'],
['age', 25],
['city', '北京'],
])
// keys()
for (const key of map.keys()) {
console.log(key) // 'name', 'age', 'city'
}
// values()
for (const value of map.values()) {
console.log(value) // '张三', 25, '北京'
}
// entries()
for (const [key, value] of map.entries()) {
console.log(key, value)
}
// forEach
map.forEach((value, key, map) => {
console.log(key, value)
})
// for...of(默认使用 entries())
for (const [key, value] of map) {
console.log(key, value)
}

转换#

const map = new Map([
['a', 1],
['b', 2],
])
// Map 转数组
;[...map] // [['a', 1], ['b', 2]]
;[...map.keys()] // ['a', 'b']
;[...map.values()] // [1, 2]
// Map 转对象
Object.fromEntries(map) // { a: 1, b: 2 }
// 对象转 Map
const obj = { x: 1, y: 2 }
new Map(Object.entries(obj)) // Map {'x' => 1, 'y' => 2}

Map vs Object#

特性MapObject
键类型任意类型字符串或 Symbol
顺序保持插入顺序基本保持(但有特殊规则)
大小map.sizeObject.keys(obj).length
遍历直接可迭代需要 Object.keys/entries
性能频繁增删更优少量数据更优
原型链无原型污染可能继承属性
// Object 的键只能是字符串
const obj = {}
obj[1] = 'one'
obj['1'] = 'string one'
console.log(obj[1]) // 'string one'(被覆盖)
// Map 区分类型
const map = new Map()
map.set(1, 'one')
map.set('1', 'string one')
console.log(map.get(1)) // 'one'
console.log(map.get('1')) // 'string one'

实际应用#

缓存#

const cache = new Map()
function memoize(fn) {
return function (...args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
return cache.get(key)
}
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
const expensiveFn = memoize((n) => {
console.log('Computing...')
return n * n
})
expensiveFn(5) // 'Computing...' 25
expensiveFn(5) // 25(从缓存读取)

计数器#

function countOccurrences(arr) {
const counts = new Map()
for (const item of arr) {
counts.set(item, (counts.get(item) || 0) + 1)
}
return counts
}
const words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
const counts = countOccurrences(words)
// Map {'apple' => 3, 'banana' => 2, 'cherry' => 1}

DOM 元素关联数据#

const elementData = new Map()
function setData(element, key, value) {
if (!elementData.has(element)) {
elementData.set(element, new Map())
}
elementData.get(element).set(key, value)
}
function getData(element, key) {
return elementData.get(element)?.get(key)
}
const button = document.querySelector('button')
setData(button, 'clicks', 0)
setData(button, 'lastClick', null)

双向映射#

class BiMap {
#forward = new Map()
#reverse = new Map()
set(key, value) {
this.#forward.set(key, value)
this.#reverse.set(value, key)
}
get(key) {
return this.#forward.get(key)
}
getKey(value) {
return this.#reverse.get(value)
}
delete(key) {
const value = this.#forward.get(key)
this.#forward.delete(key)
this.#reverse.delete(value)
}
}
const userIds = new BiMap()
userIds.set('user_001', '张三')
userIds.get('user_001') // '张三'
userIds.getKey('张三') // 'user_001'

WeakMap#

WeakMap 与 Map 类似,但键必须是对象,且是弱引用。

特点#

  1. 键必须是对象
  2. 键是弱引用(不阻止垃圾回收)
  3. 不可遍历(没有 size、keys、values 等)
const wm = new WeakMap()
const key = { id: 1 }
wm.set(key, 'value')
console.log(wm.get(key)) // 'value'
console.log(wm.has(key)) // true
wm.delete(key)
console.log(wm.has(key)) // false
// 不能使用原始值作为键
// wm.set('string', 'value'); // TypeError
// wm.set(1, 'value'); // TypeError

WeakMap 应用场景#

私有属性#

const privateData = new WeakMap()
class Person {
constructor(name, age) {
privateData.set(this, { name, age })
}
get name() {
return privateData.get(this).name
}
get age() {
return privateData.get(this).age
}
birthday() {
privateData.get(this).age++
}
}
const person = new Person('张三', 25)
console.log(person.name) // '张三'
console.log(person.age) // 25
person.birthday()
console.log(person.age) // 26
// 外部无法访问 privateData

DOM 节点关联数据#

const nodeData = new WeakMap()
function processNode(node) {
if (nodeData.has(node)) {
return nodeData.get(node)
}
const data = {
processed: true,
timestamp: Date.now(),
}
nodeData.set(node, data)
return data
}
// 当 DOM 节点被移除时,关联数据自动被垃圾回收

缓存计算结果#

const resultsCache = new WeakMap()
function heavyComputation(obj) {
if (resultsCache.has(obj)) {
console.log('From cache')
return resultsCache.get(obj)
}
console.log('Computing...')
const result = /* 复杂计算 */ Object.keys(obj).length
resultsCache.set(obj, result)
return result
}
const data = { a: 1, b: 2, c: 3 }
heavyComputation(data) // 'Computing...' 3
heavyComputation(data) // 'From cache' 3
// 当 data 不再被引用时,缓存自动清除

事件监听器管理#

const listeners = new WeakMap()
function addListener(element, event, handler) {
if (!listeners.has(element)) {
listeners.set(element, new Map())
}
const elementListeners = listeners.get(element)
if (!elementListeners.has(event)) {
elementListeners.set(event, new Set())
}
elementListeners.get(event).add(handler)
element.addEventListener(event, handler)
}
function removeAllListeners(element) {
const elementListeners = listeners.get(element)
if (!elementListeners) return
for (const [event, handlers] of elementListeners) {
for (const handler of handlers) {
element.removeEventListener(event, handler)
}
}
listeners.delete(element)
}

Map vs WeakMap 对比#

特性MapWeakMap
键类型任意仅对象
引用类型强引用弱引用
可遍历
size 属性
垃圾回收手动管理自动清理

小结#

方法/属性说明
new Map()创建空 Map
set(key, value)设置键值
get(key)获取值
has(key)检查键是否存在
delete(key)删除键值对
clear()清空
size键值对数量
keys()/values()/entries()遍历器
forEach()遍历

Map 适合需要任意类型键的场景,WeakMap 适合需要避免内存泄漏的对象关联数据存储。