JavaScript是动态语言,执行效率低于静态语言。V8引入了隐藏类来提高对象属性的访问速度,引入了内联缓存来加速操作。本文重点介绍V8中的隐藏类如何提高访问对象属性值的速度。为什么静态语言效率更高?由于隐藏类借用了静态语言的一些特性,为了把这个问题解释清楚,我们先来分析一下为什么静态语言比动态语言效率更高。我们通过下面两段代码来比较一下动态语言和静态语言在运行时的一些特点。一种是动态语言的JavaScript,一种是静态语言的C++源码。具体源码可以参考下图:这两段代码在执行过程中有什么区别?当JavaScript运行时,可以修改对象的属性。V8在使用start.x的时候并不知道start中有没有x,也不知道x相对于对象的偏移量。也可以说V8并不知道物体的具体形状。V8会根据特定规则逐级查询。这个过程很慢很费时间(《V8是怎样提升对象属性访问速度的:快属性和慢属性》)。C++,一种静态语言,是不同的。在声明对象之前,C++需要定义对象的结构(也称为形状)。比如Point结构是一个shape,然后用这个结构(shape)来定义具体的对象。C++代码在执行前需要编译。编译时,每个对象的形状是固定的。也就是说,Point的形状在代码执行过程中是不能改变的。那么在C++中访问对象的某个属性时,自然会知道该属性相对于对象地址的偏移值。例如,在C++中使用start.x时,编译器会直接将x相对于start的地址写入汇编指令中。当在objectstart中使用x属性时,CPU可以直接从内存地址取内容,即Yes,不需要任何中间查找。因为在静态语言中,可以通过偏移查询直接查询对象的属性值,这也是静态语言执行效率高的原因之一。什么是隐藏类(HiddenClass)?V8借鉴了这个静态特性,实现思路是让JavaScript中的对象静态化:V8在运行JavaScript时会假设JavaScript中的对象是静态的。具体来说,V8对每个对象做了以下两个假设:对象创建后不会再添加新的属性;创建对象后不会删除属性。满足这两个假设后,V8就可以对JavaScript中的对象进行深度优化。V8会为每个对象创建一个隐藏类。对象的隐藏类记录了对象的一些基本布局信息,包括以下两点:对象包含的所有属性;每个属性相对于对象的偏移量。有了隐藏类,当V8访问对象中的某个属性时,会先去隐藏类中寻找该属性相对于其对象的偏移量。有了偏移量和属性类型,V8就可以直接从内存中获取对象的属性值,而不需要经过一系列的查找过程,大大提高了V8查找对象的效率。我们可以结合一段代码来分析隐藏类的工作原理:letpoint={x:100,y:200}当V8执行这段代码时,会先为point对象创建一个隐藏类。在V8中,隐藏类又称为map,每个对象都有一个map属性,其值指向内存中的隐藏类。隐藏类描述了对象的属性布局,主要包括属性名和每个属性对应的偏移量。例如点对象的隐藏类包括x和y属性,x的偏移量为4,y的偏移量为8。注意这是点对象的映射,不是点对象本身.关于点对象和地图的关系,可以参考下图:这张图中,左边是点??对象在内存中的布局,右边是点对象的地图。我们可以看到点对象的第一个属性是指向它的地图。有了地图后,再次使用point.x访问x属性时,V8会查询点地图中x属性相对于点对象的偏移量,然后将偏移量加到点对象的起始位置,就会得到x属性的值在内存中的位置是已知的,有了这个位置,就得到了x的值,这样我们就省去了比较复杂的查找过程。这是一个让动态语言变成静态的操作。V8引入隐藏类来模拟C++等静态语言的机制,从而达到静态语言的执行效率。多个对象共享一个隐藏类在V8中,每个对象都有一个map属性,其值指向对象的隐藏类。但是,如果两个对象的形状相同,V8会为它们重用同一个隐藏类,这有两个好处:减少隐藏类的创建次数,间接加快代码执行速度;减少隐藏类存储的数量。什么情况下两个对象的形状相同,满足以下两点:属性名相同;相同数量的属性。可以参考下面的代码:letpoint={x:100,y:200};让point2={x:3,y:4};V8在执行这段代码时,会先为point对象创建一个隐藏类,然后继续创建point2对象。在创建point2对象的过程中,发现它的shape和point是一样的。这时V8会复用pointtopoint2的隐藏类。具体效果可以参考下图:重建隐藏类V8实现隐藏类需要两个假设:创建对象后,没有newAttributes;创建对象后不会删除属性。然而,JavaScript仍然是一种动态语言。在执行期间,可以更改对象的形状。如果对象的形状发生变化,隐藏类也会发生变化。这意味着V8需要重新构造一个新的隐藏类,这对于V8的执行效率来说是一个很大的开销。通俗地说,给一个对象增加新的属性,删除新的属性,或者改变一个属性的数据类型都会改变对象的形状,而形状改变后必然会触发V8为对象重建一个新的隐藏类.我们可以看一个简单的例子:letpoint={};观点。就是说隐藏类也改变了,改变过程可以参考下图:同理,如果删除对象的某个属性(代码如下),那么对象的形状也会改变,V8也会改变会重建对象的隐藏类:letpoint={x:100,y:200};deletepoint.xbestpracticeV8会给每个对象分配一个隐藏类,在执行过程中:如果对象的形状不会发生如果对象发生变化,对象将始终使用隐藏类;如果对象的形状发生变化,V8将为该对象重建一个新的隐藏类。为了避免触发V8重构对象的隐藏类,尽量注意以下几点:使用字面量初始化对象时,保证属性顺序一致。例如先通过字面量x和y的顺序创建一个点对象,然后通过字面量y和x的顺序创建一个对象point2,代码如下:letpoint={x:100,y:200};让point2={y:100,x:200};虽然创建时对象属性相同,但它们的初始化顺序不同,也会导致形状不同,因此它们会有不同的隐藏类,应尽量避免。尝试使用字面值一次性初始化完整的对象属性。因为每次为对象添加属性时,V8都会为该对象重置隐藏类。尽量避免使用delete方法。delete方法破坏了对象的形状,这也导致V8为对象重新生成一个新的隐藏类。综上所述,在V8中,每个对象都有一个隐藏的类映射,它描述了它的对象的内存布局,比如对象中包含了哪些属性,这些数据对应对象的偏移量是多少。V8可以根据隐藏类中描述的偏移地址获取对应的属性值,省去了复杂的查找过程。隐藏类基于两个假设:创建对象后,不会添加新的属性;对象创建后,属性不会被删除。一旦物体的形状发生变化,V8需要为物体重建一个新的隐藏类,所以尽量不要在程序中随意改变物体的形状。
