最近在 EventEmitter3 源码 中看到了 Object.create(null)
,做一下考证。
在 JavaScript 中,通常使用对象字面量语法来创建空对象。
var foo = {}; // create new object foo.bar = 'bar'; // string -> string foo.baz = function () {}; // string -> function
然而, {}
并非真正的「空」对象:它通过原型链继承了如 hasOwnProperty()
、 toString()
、 valueOf()
、 constructor
、 __proto__
等 Object 对象 的属性和方法。
var foo = {}; foo.toString(); // "[object Object]" foo.valueOf(); // Object {}
{}
创建的对象,其原型对象指向 Object.prototype
(读者可以在 Chrome 控制台输入 {}
并回车,查看显示的结果)。
var foo = {}; Object.prototype.isPrototypeOf(foo); // true foo instanceof Object // true foo.constructor === Object // true
换言之, {}
对象字面量等价于 Object.create(Object.prototype)
:
var foo = Object.create(Object.prototype);
糟糕的是,这些继承的属性是隐藏的,即不可 enumerable
。
foo.propertyIsEnumerable('toString'); // false for (var property in foo) { console.log(property); } // nothing is printed
即便是 propertyIsEnumerable
也不可枚举自身。
foo.propertyIsEnumerable('propertyIsEnumerable'); // false
当然,这样设计的初衷是为了区分自有属性和继承自原型链的属性,但 JavaScript 对象有时通过原型链方法进行隐式类型转型,颇令人费解,进而招致骂名。
{}.length // SyntaxError: Unexpected token . ({}).length // undefined {} + 1 // 1 {} + {} // Safari/ Firefox: NaN; Chrome/Node: "[object Object][object Object]" ({} + 1).length // 16
({} + 1).length
为什么返回 16
呢?
valueOf()
, {}.valueOf()
通过 Object.prototype.valueOf()
返回其自身; toString()
。 {}.toString()
通过 Object.prototype.toString()
返回 "[object Object]"
,字符串与 1
相加,得到 "[object Object]1"
,其长度为 16
。 我们可以通过改写原型方法来追踪此过程( 仅作演示,实践中请勿改写内置对象的原型方法 ):
Object.prototype.valueOf = function() { console.log('valueOf called'); return this; }; Object.prototype.toString = function() { console.log('toString called'); return 'xyz'; }; ({} + 1).length; // 4 // valueOf called // toString called // 'xyz1'.length === 4
有可能创建没有继承 Object 原型的、「纯粹」的空对象呢?答案是肯定的。
根据 《You Don't Know JS: this & Object Prototypes》 描述:
Object.create(null)
is similar to {}
, but without the delegation to Object.prototype
, so it's "more empty" than just {}
.
var foo = Object.create(null); foo instanceof Object // false foo.constructor // undefined foo.toString(); // TypeError: Object [object Object] has no method 'toString'
Object.create 的内部实现如下:
Object.create = function(o) { function F() {} F.prototype = o; return new F(); };
「纯粹」的对象适合用于存储键值对数据,而且没有隐式的类型转换,更加直观。
var foo = Object.create(null); foo + foo // TypeError: Cannot convert object to primitive value (foo + 1).length; // TypeError: Cannot convert object to primitive value
当然,纯粹对象仅仅是没有继承 Object 原型的属性和方法,其他和普通对象并无二致。
var foo = Object.create(null); foo.bar = 'bar'; foo['bar'] // returns 'bar' Object.freeze(foo); foo.baz = 'baz'; // trying to add new property // either does nothing silently // or throws TypeError in strict mode foo.getBar = function() { return this.bar; } foo.getBar(); // returns value 'bar'
因为没有继承,使用 for-in
循环的时候也就不用再检查 hasOwnProperty
了。
var foo = Object.create(null); foo.bar = 'baz'; for (var property in foo) { console.log(property); } // prints bar
纯粹 JavaScript 对象在 《Speaking JavaScript》一书中被称为「字典模式」 。
另外,使用 JSON.parse
创建的对象并不「纯粹」:
var foo = JSON.parse('{}'); foo instanceof Object // true
{}
和 Object.create(null)
性能比较如下:
{}
比 Object.create(null)
快 20 倍 ,如果创建很多对象,就需要留意一下。猜测原因可能是: {}
空对象只是内存分配和拷贝预填充的对象元数据,而 Object.create(null)
实际上是执行自定义函数创建、填充对象。 JSON.stringify
速度:序列化纯粹对象快 大约 3% 左右 ,几乎可以忽略。 ES2015 允许通过 __proto__
属性设置原型对象 ,纯粹对象可以设置 __proto__
,而且显示指向了设置的对象(不同 JavaScript 引擎可能会有差异),但是并没有像预期那样运行。因此,需要设置对象原型时,不能使用纯粹对象。
var foo = Object.create(null); foo.__proto__ = { bar: 'bar' }; foo; // { [__proto__]: { bar: 'bar' } } foo.bar; // undefined
Object.create(null)
虽然提供了一种创建「纯粹」对象的方式,但综合性能和兼容问题,似乎找不到太多用的理由。