Skip to content

Object.create(null) 的特殊意义:为何它是“最干净”的对象?在 Map 前时代如何模拟哈希表?

Object.create(null) 不是创建一个空对象,而是创建一个‘无继承’的对象——它切断了原型链的源头。”

当你写下:

js
const obj = {};

你得到的不是一个“纯粹”的键值容器,
而是一个继承自 Object.prototype 的对象,它自带 toStringhasOwnProperty 等方法。

而当你写下:

js
const clean = Object.create(null);

你得到的是一个完全没有原型的对象——
它不继承任何属性,不包含任何“默认行为”。

我们来从 原型机制、内部槽、属性查找协议、历史用途 四个维度,彻底拆解:

一、{}Object.create(null) 的本质差异

{} 的原型链

js
const obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
console.log('toString' in obj); // true

{} 等价于:

js
Object.create(Object.prototype)

这意味着:

  • obj[[Prototype]] 指向 Object.prototype
  • 所有从 Object.prototype 继承的方法都可通过原型链访问
  • 它是一个“完整”的对象,但带有“噪音”

Object.create(null) 的原型链

js
const clean = Object.create(null);
console.log(Object.getPrototypeOf(clean)); // null
console.log('toString' in clean); // false
console.log(clean.toString); // undefined

clean[[Prototype]] 被显式设置为 null
因此它的原型链在第一步就终止了。

这是 JavaScript 中唯一能创建“无原型对象”的标准方式。

二、规范定义:Object.create 如何工作?

来自 ECMA-262 §20.2.3.1

js
Object.create(proto, propertiesObject)
  • proto:新对象的 [[Prototype]]
  • 如果 proto 不是对象或 null,抛出 TypeError
  • 如果 protonull,新对象没有原型
js
// 合法
Object.create(null);
Object.create(Object.prototype);

// 非法
Object.create(1);     // TypeError
Object.create('abc'); // TypeError

三、为何它是“最干净”的对象?

“干净”意味着:

  • 没有继承任何属性
  • 不会与用户定义的键名冲突
  • 属性查找只在自身,不沿原型链向上

场景对比:hasOwnProperty 的困境

js
const obj = { hasOwnProperty: 'hacked' };
obj.hasOwnProperty('name'); // 报错:obj.hasOwnProperty is not a function

因为 obj.hasOwnProperty 被覆盖,无法调用原型上的方法。

而使用 Object.create(null)

js
const clean = Object.create(null);
clean.hasOwnProperty = 'safe';
// 仍然可以安全使用 Reflect 或 call
Object.prototype.hasOwnProperty.call(clean, 'name'); // false

更安全的方式是使用 Reflect.has

js
Reflect.has(clean, 'name'); // 推荐

四、在 Map 出现前,如何用 Object.create(null) 模拟哈希表?

在 ES6 之前,JavaScript 没有原生的 Map
开发者常用对象模拟键值存储:

js
const cache = {};
cache['key1'] = 'value1';
cache['key2'] = 'value2';

但存在严重问题:

问题 1:原型污染

js
cache['toString'] = 'evil';
// 后续遍历可能出错
for (let key in cache) {
  // toString 也会被枚举
}

问题 2:hasOwnProperty 冲突

js
if (cache.hasOwnProperty('constructor')) {
  // 即使没有显式设置,也可能因原型链返回 true
}

解法:使用 Object.create(null)

js
const safeCache = Object.create(null);
safeCache['toString'] = 'safe';
safeCache['__proto__'] = 'also safe';

// 遍历时不会受到原型干扰
for (let key in safeCache) {
  console.log(key, safeCache[key]); // 只输出 own properties
}

因为它没有原型,所以:

  • 不会继承 Object.prototype 的可枚举属性
  • 所有键都是“纯净”的用户数据
  • for...in 遍历更安全

五、属性查找协议:为何 in 操作符在 null 原型对象上更可靠?

来自 ECMA-262 §13.10.1

in 操作符的语义是: “检查一个属性名是否存在于对象或其原型链中。”

但对于 Object.create(null)

  • 原型链为 null
  • 因此 in 只检查对象自身的所有属性(包括不可枚举的)
js
const clean = Object.create(null);
clean.x = 1;

'x' in clean;        // true
'y' in clean;        // false
'toString' in clean; // false

而普通对象:

js
const obj = {};
'toString' in obj;   // true(来自 Object.prototype)

所以:Object.create(null) 上的 in 检查是“精确”的,不会因原型链产生误判。

六、实际应用场景

1. 构建私有命名空间

js
const privateState = Object.create(null);
privateState.secret = 'shh';
// 外部无法通过原型链污染

2. 配置映射表

js
const mimeTypes = Object.create(null);
mimeTypes['.js'] = 'application/javascript';
mimeTypes['.json'] = 'application/json';

避免 hasOwnProperty 冲突。

3. 模拟 Map(ES6 前)

js
function createMap() {
  const data = Object.create(null);
  return {
    set(key, value) { data[key] = value; },
    get(key) { return data[key]; },
    has(key) { return key in data; },
    delete(key) { delete data[key]; }
  };
}

虽然不如 Map 支持任意键类型,但对于字符串键是安全的。

七、与 Map 的对比:为何 Map 更优?

尽管 Object.create(null) 是“最干净”的对象,
Map 在以下方面更胜一筹:

特性Object.create(null)Map
键类型只能是 string / symbol任意类型(对象、函数等)
性能属性查找 O(1) 但受引擎优化限制专为键值存储优化
内存可能被原型污染(罕见)完全隔离
方法需手动实现 sizeclear内置丰富 API
遍历for...in 需注意枚举性for...of 原生支持

现代开发应优先使用 MapObject.create(null) 仅用于特定场景。

一句话总结

Object.create(null) 创建的是一个 [[Prototype]]null 的对象,它不继承 Object.prototype 的任何属性,因此是 JavaScript 中“最干净”的对象。

  • 它切断了原型链,避免了 hasOwnProperty 冲突和原型污染
  • Map 出现前,它是构建安全哈希表的首选方案
  • 它的 in 操作符查找是精确的,不涉及原型链
  • 尽管现代应用中 Map 更优,但在需要轻量级、无继承对象时仍有价值

结语:理解 Object.create(null),就是理解“原型链的边界”

大多数人认为 {} 是“空对象”,
而你理解的是:

{} 是一个继承自 Object.prototype 的对象,而 Object.create(null) 是一个完全独立的、无父的对象——它是 JavaScript 类型系统中的一块‘飞地’。”

你不再只是“创建对象”,
而是在精确控制原型链的拓扑结构

下一站,我们可以深入:

属性描述符与不可变性:configurablewritableenumerable——如何真正‘冻结’一个对象?

继续这场从表象到协议的旅程,
你正在掌握 JavaScript 对象系统的底层控制权。