Skip to content

Instantly share code, notes, and snippets.

@yyx990803
Last active March 28, 2017 11:39
Show Gist options
  • Save yyx990803/6246816 to your computer and use it in GitHub Desktop.
Save yyx990803/6246816 to your computer and use it in GitHub Desktop.
关于应不应该把属性的默认值放在prototype里

TL,DR: 务必总是在构建函数里定义实例属性。

在小胖的PoorPhy物理库里,有很多类似这样的代码:

function WorldA () {
  // ...
}

WorldA.prototype = {
  gravityX: 0,
  gravityY: 0,
  // ...
}

相比之下,另一种写法是在构建函数里定义属性:

function WorldB () {
  this.gravityX = 0
  this.gravityY = 0
}

不难看出,gravityXgravityY都应该是实例属性,也就是说每个World对象的这两个属性应该是会不一样的。把这些属性放在原型里,创建实例对象以后,实例对象本身的这两个属性其实是空的,如果在实例上不定义gravityX,则我们获得的其实是原型里面的gravityX

var world = new WorldA()
world.gravityX // 0
world.hasOwnProperty('gravityX') // false

可能大家会觉得第一种写法没什么不好,反正实例上没定义的话还是能获得默认值啊,还省去了构建函数里的定义操作,没定义的属性还省内存。可是其实这样对性能是会有影响的。这里涉及到JavaScript引擎的内部优化。以V8和如下代码为例:

function reverseGravity (world) {
  world.gravityX = -world.gravityX
  world.gravityY = -world.gravityY
}

大家可能听说过hidden class的说法。在V8中,只有当两个对象本身的所有属性按顺序从名字到类型都完全相同时,他们才拥有相同的hidden class。hidden class类似于C的struct,因为明确知道内存的分布,所以V8生成的机器码在操作对象时的指令数就可以很少,效率也会很高。

reverseGravity这个函数里,如果每次获得的world对象都在自身上具有gravityXgravityY,那么V8在编译这个函数的时候,就会预设每次遇到的都是同一个hidden class,因此这个函数对应的机器码效率就会很高。但是!如果world对象有时候gravityX是在自己身上的,有时候是在原型上的(自身没定义),V8就需要进行一次额外的hidden class check,效率就会低一些。

到这里,性能上的损失其实还可以接受。但是!假如world时有时无的属性超过了4个(也就是可能会有4个以上的不同的hidden class),V8就会改用hash table而不是hidden class来检索对象上的属性了!(为啥是4个?源码是这么写的 -_-)hash table查找的机器码指令数比起hidden class来说,多了不是一点点。例子说话,差距有多少,看这个jsPerf,性能差距高达80%。

以上所描述的问题,不仅仅在V8中存在。现代JS引擎所使用的优化技巧都很类似,在其他JS引擎中,两种情况也有很大的性能差异,只是没有在V8下那么极端而已。类似地,为了防止hidden class的改变,慎用delete。

附:对比V8在两种情况下产生的机器码

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment