TL,DR: 务必总是在构建函数里定义实例属性。
function WorldA () {
// ...
}
WorldA.prototype = {
gravityX: 0,
gravityY: 0,
// ...
}
相比之下,另一种写法是在构建函数里定义属性:
function WorldB () {
this.gravityX = 0
this.gravityY = 0
}
不难看出,gravityX
和gravityY
都应该是实例属性,也就是说每个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对象都在自身上具有gravityX
和gravityY
,那么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。