Skip to content

Object.create(null)

前言

vuejs/core 的代码中看到这样的一个 PR,仅仅只修改了一行代码:

ts
// instance.accessCache = {}
instance.accessCache = Object.create(null)

下面来探讨下为什么要这么做。

Object.create()的定义

照搬一下 mdn 上的定义:

ts
Object.create(proto,[propertiesObject])
  • proto: 新创建对象的原型对象
  • propertiesObject: 可选。要添加到新对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)。

使用 null 原型的对象

在控制台中我们可以明显的看到 {}Object.create(null) 的区别:

null 为原型的对象存在不可预期的行为,因为它未从 Object.prototype 继承任何对象方法。特别是在调试时,因为常见的对象属性的转换/检测工具可能会产生错误或丢失信息(特别是在静默模式,会忽略错误的情况下)。

例如:

ts
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 方法,类似于这样:

ts
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 原型自有的属性的存在会导致一些错误:

ts
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 为原型的对象消除了这种潜在的问题,且不会给 hasPersongetAge 函数引入太多复杂的逻辑:

ts
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 为原型的对象则不受影响。

ts
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 来使用,我们可以自己定义hasOwnPropertytoString方法,不管是有意还是不小心,我们完全不必担心会将原型链上的同名方法覆盖掉。

另一个使用 create(null) 的理由是,在我们使用 for..in 循环的时候会遍历对象原型链上的属性,使用 create(null) 就不必再对属性进行检查了,当然,我们也可以直接使用 Object.keys[]

总结

使用 Object.create(null) 替代 {} 的场景:

  1. 需要一个非常干净且高度可定制的对象用作数据字典的时候;
  2. 预防第三方库等因素造成原型链注入污染;
  3. 想节省 hasOwnProperty 带来的性能损失;

使用 Object.create(null) 需要注意:

  1. 由于没有原型链方法,因此无法进行默认的类型转换(也就是无法调用toString方法导致的 console/字符串拼接 时报错)

参考