/ JavaScript

纯粹的 JavaScript 对象

最近在 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 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) 虽然提供了一种创建「纯粹」对象的方式,但综合性能和兼容问题,似乎找不到太多用的理由。

参考链接