先记住这个答案
普通对象读属性时,自己身上没有就会去原型链上找。所以一个空对象 dict 读 dict['toString'] 会拿到 Object.prototype 上的方法,拿它判断「用户有没有存过 toString 这个键」就错了。Map 的 get 和 has 只在自己存进去的条目里查,原型上的方法不会被当成键,所以没有这个问题。对象方案也能补救,用 Object.hasOwn 判断自有属性,或者用 Object.create(null) 创建一个没有原型的对象。另外,判断键是否存在和取值要分开,值可能本来就是 0 或 undefined。
obj[key]会查原型链,能读到toStringMap的条目和它自身的属性是两套存储- 判断键是否存在用
has或Object.hasOwn - 值可能是 0 或
false,别拿真假判断存在
空对象为什么能读到 toString
做一个单词计数功能,用户输入的单词当键。用户恰好输入了 toString 或 constructor,普通对象就出问题了。
Map 对照JavaScriptconst dict = {}
const map = new Map()
console.log(typeof dict.toString, Object.hasOwn(dict, 'toString')) // 读到了继承的方法
console.log(map.has('toString')) // Map 里没有这个条目
map.set('toString', undefined) // 存一个值为 undefined 的条目
console.log(map.has('toString'), map.get('toString') === undefined)
map.extra = 1 // 给 Map 对象挂一个普通属性
console.log(map.has('extra'), map.size) // 不是条目,size 也不算查看输出与解释
function false
false
true true
false 1第一行说明 dict.toString 能读到一个函数,但 Object.hasOwn 返回 false,这个函数是从原型继承来的,不是字典里存的。Map 没有这个问题,没 set 过的键 has 就是 false。
最后一行说明 Map 有两套数据:set 存进去的条目,和它作为对象身上的普通属性。map.extra = 1 只是加了一个属性,has 查不到,size 也不算。用 Map 就只用 set、get 和 has 这些方法,不要混用方括号。
用对象当字典时的两种补救
如果键都是字符串,又不想换成 Map,普通对象也能安全地用,关键是不要走原型链。
Object.hasOwn和无原型对象- 判断存在用
Object.hasOwn(dict, key),它只看对象自己的属性。老写法dict.hasOwnProperty(key)有隐患:如果用户存了一个叫hasOwnProperty的键,这个方法就被覆盖了。 另一种办法是Object.create(null)创建字典,它没有原型,读toString就是undefined。但它也就没有hasOwnProperty这些方法可以调用了,判断存在还是用Object.hasOwn或in。
容易答错的地方
- 用
if (cache[key])判断缓存命中 - 这个写法有两个问题。一是会命中原型上的属性;二是值为 0、
false、空字符串时会被当成没命中,于是每次都重新计算。应该先判断键存不存在,存在再取值。 - 以为
map.get(key)返回undefined就是没存过 Map允许存undefined作为值,这时get的结果和没存过一样。缓存里需要区分「算过但结果为空」和「没算过」,要用has。- 以为换成
Map就杜绝了原型污染 Map只解决了它自己的查询问题。如果代码其他地方还在把用户输入深度合并到普通对象里,比如处理__proto__这样的键,原型污染风险照样存在,要在合并逻辑里过滤这些危险键。
面试官还会怎么问?
Object.create(null) 和 Map 是一回事吗?
不是。Object.create(null) 仍然是普通对象,键只能是字符串或 Symbol,没有 size,统计数量要用 Object.keys。Map 的键可以是任意值,包括对象,还自带 size 和按插入顺序遍历。
in 和 Object.hasOwn 有什么区别?
key in obj 会查原型链,'toString' in {} 是 true。Object.hasOwn(obj, key) 只查对象自己,Object.hasOwn({}, 'toString') 是 false。字典场景要用后者。
map.get 读不存在的键会报错吗?
不会,返回 undefined。想在没有时给默认值,可以写 map.has(key) ? map.get(key) : defaultValue;如果确定不会存 undefined,用 map.get(key) ?? defaultValue 更简洁。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。