最近在 EventEmitter3 源码 中看到了 Object.create(null)
,做一下考证。
传统的 JavaScript 对象
在 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()
返回其自身; - 又回到了对象加数字 1,尝试第二种方式:使用
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
纯粹的 JavaScript 对象
有可能创建没有继承 Object 原型的、「纯粹」的空对象呢?答案是肯定的。
根据 《You Don't Know JS: this & Object Prototypes》描述:
Object.create(null)
is similar to{}
, but without the delegation toObject.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)
虽然提供了一种创建「纯粹」对象的方式,但综合性能和兼容问题,似乎找不到太多用的理由。