先记住这个答案
普通对象的键只能是字符串或 Symbol,其他类型的值当键时都会先被转成字符串。所以 obj[1] 和 obj['1'] 是同一个属性,拿对象当键会变成 "[object Object]",不同的对象会互相覆盖。Map 的键可以是任意类型,包括对象、函数、NaN,存进去是什么就是什么,对象按引用区分。所以当键不是字符串,比如要给 DOM 节点或对象关联数据时,就需要 Map。另外 Map 自带 size、按插入顺序遍历、不会和原型上的属性冲突,频繁增删的场景也更合适。
- 对象的键会被转成字符串,Symbol 除外
- 拿对象当键,全都变成
"[object Object]" Map的键保留原类型,对象按引用区分- 键不确定、频繁增删、要计数时用
Map
同样的键,存进两种容器
分别用一个对象、另一个结构类似的对象、数字 1 和字符串 '1' 当键,看看两种容器里各存了几个。
const user = { id: 1 }
const obj = {}
obj[user] = 'a' // 键被转成 '[object Object]'
obj[{ id: 2 }] = 'b' // 也是 '[object Object]',覆盖了上一个
obj[1] = 'one' // 键被转成 '1'
console.log(JSON.stringify(Object.keys(obj)))
console.log(obj['1'] === obj[1])
const map = new Map()
map.set(user, 'a').set({ id: 2 }, 'b').set(1, 'one').set('1', 'string one')
console.log(map.size) // 四个键互不影响
console.log(map.get(user), map.get(1), map.get('1'))查看输出与解释
["1","[object Object]"]
true
4
a one string one普通对象里只剩两个键。两个不同的对象都被 String() 转成了 "[object Object]",第二次赋值把第一次覆盖了;数字 1 被转成了 "1",所以 obj['1'] 和 obj[1] 读到的是同一个值。Map 里四个键都在,user 这个对象本身就是键,拿同一个引用就能取到值。
什么时候用 Map,什么时候用对象
最典型的 Map 场景是给对象关联额外数据。比如记录每个 DOM 节点被点击了几次,或者缓存某个对象的计算结果,键就是对象本身。用普通对象根本做不到,只能给每个对象编一个字符串 id。
- 其他适合
Map的情况 - 键来自用户输入时,普通对象会和原型上的
toString、constructor这些属性冲突,Map没有这个问题。需要频繁增删键、随时知道有多少个键时,Map有size,引擎对频繁增删也做了优化。需要按插入顺序遍历时,Map严格保证顺序,对象会把整数键挪到前面。 反过来,结构固定的数据,比如一个用户的name、age字段,用对象更自然。需要直接JSON.stringify发给后端时,对象也更方便,Map要先转成数组。
容易答错的地方
- 以为对象的键可以是数字
obj[1] = 'x'看起来键是数字,其实存进去的是字符串"1",调用Object.keys(obj)返回的是["1"]。数组的索引也一样,本质上都是字符串键。- 以为
Map会按对象内容比较键 map.set({ id: 1 }, 'a')之后,map.get({ id: 1 })是undefined,因为这是两个不同的对象。要按内容查找,就用内容里的某个稳定字段当键,比如map.set(user.id, user)。
面试官还会怎么问?
Map 和 WeakMap 的键有什么区别?
Map 的键可以是任意值,而且会一直引用着键,对象键不会被垃圾回收。WeakMap 的键只能是对象或非注册的 Symbol,对键是弱引用,键对象没有别的地方引用时,这个条目会被自动回收,但它不能遍历,也没有 size。
NaN 能当 Map 的键吗?
能。Map 用 SameValueZero 规则比较键,NaN 被认为等于 NaN,所以 map.set(NaN, 1) 之后 map.get(NaN) 能取到 1。+0 和 -0 也被当作同一个键。
对象和 Map 怎么互相转换?
对象转 Map 用 new Map(Object.entries(obj)),反过来 Map 转对象用 Object.fromEntries(map)。反向转换时非字符串的键会被转成字符串,对象键会丢失,所以只适合键都是字符串的 Map。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。