先记住这个答案
Object.create(null) 返回的对象没有原型,它的 [[Prototype]] 是 null。平时对象身上的 toString、valueOf、hasOwnProperty、isPrototypeOf 都来自 Object.prototype,这个对象一个都继承不到。直接调用这些方法会报 TypeError,放进模板字符串或 String() 里也会报错,因为转字符串时找不到 toString 和 valueOf。它的用处是当纯字典,键名随便起,不会和原型上的属性冲突。
- 空原型对象没有继承方法
- 空原型对象直接转字符串会报错
- 判断自有键用
Object.hasOwn,别调实例方法 - 适合当字典,
in不会误命中toString
实际跑一下看哪些会失败
平时写 {},等价于 Object.create(Object.prototype)。把原型换成 null,差别马上就出来了。
const dict = Object.create(null)
dict.name = 'Tom'
console.log(Object.getPrototypeOf(dict)) // null
console.log(typeof dict.toString) // undefined,没继承到
console.log('toString' in dict) // false,普通对象这里是 true
console.log(Object.hasOwn(dict, 'name')) // true,静态方法照常用
console.log(dict instanceof Object) // false,但不报错
try {
dict.hasOwnProperty('name')
} catch (e) {
console.log(e.name) // 方法不存在
}
try {
console.log(`${dict}`)
} catch (e) {
console.log(e.name) // 转不成字符串
}查看输出与解释
null
undefined
false
true
false
TypeError
TypeError规律很好记:凡是「对象自己身上调用」的方法都失效,凡是 Object.xxx(obj) 这种静态方法都正常。Object.keys、Object.entries、JSON.stringify 都能用,因为它们不需要从对象的原型上找东西。
${dict} 报错的原因是,转原始值时引擎会依次找 Symbol.toPrimitive、valueOf、toString,三个都没有,只能抛 TypeError。在控制台里打印没问题,但写日志时拼字符串就会炸。
为什么要专门造一个没原型的对象
拿普通对象当字典,有个隐藏的坑。比如统计单词出现次数,用 counts[word] 存。碰到单词 constructor 时,counts['constructor'] 一开始就不是 undefined,而是 Object 函数,计数逻辑就乱了。用 in 判断键是否存在也一样,'toString' in {} 是 true。
Object.create(null) 把这个问题从根上去掉了:没有原型,就没有任何「天生就有」的键。所以不少库内部的缓存表都这样创建。
它还能挡一部分原型污染。如果有人往 Object.prototype 上加了属性,普通对象都能读到,无原型对象读不到。另外,在它身上写 dict.__proto__ = x 不会改原型,这里的 __proto__ 只是一个普通键。因为 __proto__ 的 getter 和 setter 定义在 Object.prototype 上,这个对象继承不到,写进去就是个叫 __proto__ 的普通属性。
- 和 Map 怎么选
- 新代码当字典,
Map通常更合适:键可以是任意类型,有size,按插入顺序遍历,也没有原型冲突。无原型对象的优势是能直接JSON.stringify,读写语法就是普通的点和方括号,适合要序列化或者要和现有对象接口保持一致的场景。
容易答错的地方
- 说它不能用于
instanceof dict instanceof Object完全合法,只是返回false,因为原型链上找不到Object.prototype。真正会报错的是把它放在instanceof右边,因为右边要求是函数或者带Symbol.hasInstance的对象,这和「无原型」没关系,任何普通对象放右边都报错。- 以为
Object.create(null)和{}只是少几个方法 - 少的不只是方法,还有隐式转换能力。
dict + ''、String(dict)、alert(dict)都会抛错。一些第三方库拿到对象就调obj.hasOwnProperty,传进去也会直接报错,所以对外暴露的数据最好别用它。 - 判断自有属性还写
dict.hasOwnProperty(key) - 它没有这个方法。要么用
Object.hasOwn(dict, key),要么借用Object.prototype.hasOwnProperty.call(dict, key)。其实对无原型对象来说,key in dict也等价于判断自有属性,因为原型链只有它自己一层。
面试官还会怎么问?
能不能把一个普通对象改成无原型的?
可以用 Object.setPrototypeOf(obj, null)。但修改已有对象的原型会让引擎放弃针对它的优化,性能不好,更推荐一开始就用 Object.create(null) 创建。
Object.create 的第二个参数是干什么的?
它接收一个属性描述符对象,和 Object.defineProperties 的格式一样,比如 Object.create(null, { id: { value: 1, enumerable: true } })。没写的描述符字段默认都是 false,所以这样定义的属性默认不可写、不可枚举、不可删除。
怎么让无原型对象在控制台或日志里正常显示?
最简单是 JSON.stringify(dict),或者先展开成普通对象 { ...dict } 再处理。展开运算符只复制自有可枚举属性,得到的新对象原型是 Object.prototype,所有方法又都回来了。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。