Object.create(null)
前言
在 vuejs/core
的代码中看到这样的一个 PR,仅仅只修改了一行代码:
// instance.accessCache = {}
instance.accessCache = Object.create(null)
下面来探讨下为什么要这么做。
Object.create()的定义
照搬一下 mdn 上的定义:
Object.create(proto,[propertiesObject])
- proto: 新创建对象的原型对象
- propertiesObject: 可选。要添加到新对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)。
使用 null 原型的对象
在控制台中我们可以明显的看到 {}
与 Object.create(null)
的区别:
以 null
为原型的对象存在不可预期的行为,因为它未从 Object.prototype
继承任何对象方法。特别是在调试时,因为常见的对象属性的转换/检测工具可能会产生错误或丢失信息(特别是在静默模式,会忽略错误的情况下)。
例如:
const normalObj = {}; // create a normal object
const nullProtoObj = Object.create(null); // create an object with "null" prototype
console.log("normalObj is: " + normalObj); // shows "normalObj is: [object Object]"
console.log("nullProtoObj is: " + nullProtoObj); // throws error: Cannot convert object to primitive value
alert(normalObj); // shows [object Object]
alert(nullProtoObj); // throws error: Cannot convert object to primitive value
我们可以为以 null
为原型的对象添加 toString
方法,类似于这样:
nullProtoObj.toString = Object.prototype.toString; // since new object lacks toString, add the original generic one back
console.log(nullProtoObj.toString()); // shows "[object Object]"
console.log("nullProtoObj is: " + nullProtoObj); // shows "nullProtoObj is: [object Object]"
与常规的对象不同,nullProtoObj 的 toString 方法是这个对象自身的属性,而非继承自对象的原型。这是因为 nullProtoObj “没有”原型(null
)。
在实践中,以 null
为原型的对象通常用于作为 map 的替代。因为 Object.prototype
原型自有的属性的存在会导致一些错误:
const ages = { alice: 18, bob: 27 };
function hasPerson(name) {
return name in ages;
}
function getAge(name) {
return ages[name];
}
hasPerson("hasOwnProperty") // true
getAge("toString") // [Function: toString]
使用以 null
为原型的对象消除了这种潜在的问题,且不会给 hasPerson
和 getAge
函数引入太多复杂的逻辑:
const ages = Object.create(null, {
alice: { value: 18, enumerable: true },
bob: { value: 27, enumerable: true },
});
hasPerson("hasOwnProperty") // false
getAge("toString") // undefined
在这种情况下,应谨慎添加任何方法,因为它们可能会与存储的键值对混淆。
令你使用的对象不继承 Object.prototype
原型的方法也可以防止原型污染攻击(防止evil.js
)。如果恶意脚本向 Object.prototype
添加了一个属性,这个属性将能够被程序中的每一个对象所访问,而以 null 为原型的对象则不受影响。
const user = {};
// A malicious script:
Object.prototype.authenticated = true;
// Unexpectedly allowing unauthenticated user to pass through
if (user.authenticated) {
// access confidential data...
}
Object.create(null)的使用场景
再回到文章的开头,为什么很多源码作者会使用 Object.create(null)
来初始化一个新对象呢?这是作者的习惯,还是一个最佳实践?
其实都不是,这并不是作者不经思考随便用的,也不是 javascript 编程中的最佳实践,而是需要因地制宜,具体问题具体分析。
从上节可以知道使用 create
创建的对象,没有任何属性,显示 No properties
,我们可以把它当作一个非常纯净的 map
来使用,我们可以自己定义hasOwnProperty
、toString
方法,不管是有意还是不小心,我们完全不必担心会将原型链上的同名方法覆盖掉。
另一个使用 create(null)
的理由是,在我们使用 for..in
循环的时候会遍历对象原型链上的属性,使用 create(null)
就不必再对属性进行检查了,当然,我们也可以直接使用 Object.keys[]
。
总结
使用 Object.create(null)
替代 {}
的场景:
- 需要一个非常干净且高度可定制的对象用作数据字典的时候;
- 预防第三方库等因素造成原型链注入污染;
- 想节省 hasOwnProperty 带来的性能损失;
使用 Object.create(null)
需要注意:
- 由于没有原型链方法,因此无法进行默认的类型转换(也就是无法调用
toString
方法导致的 console/字符串拼接 时报错)