隐藏类众所周知,Javascript是一种动态编程语言,这意味着对象在初始化操作后仍然可以添加或删除其属性。例如,在下面的代码中,“car”对象被初始化为两个属性“make”和“model”,但后来,该对象被动态添加了属性“year”。函数汽车(制作,模型){this.make=make;this.model=model;}constcar=newCar(honda,accord);car.year=2005;大多数Javascript解释器使用类似字典的结构来存储类的属性在内存中的位置。但是这样的结构使得Javascript在查找属性值时比Java这样的静态语言消耗更多的性能。在Java中,所有对象的属性在编译前都会由一个固定的对象结构决定,不能在运行时动态添加或删除。这样做的好处是可以将属性的值(或属性的指针)以彼此固定的偏移量存储在一个连续的内存空间中。它的偏移量可以很容易地通过属性的类型来确定,但是由于Javascript可以在运行时动态改变属性类型,所以这种方法在Javascript中是不可能的。在像Java这样的非动态语言中,单条指令可以确定属性的内存位置,但在Javascript中,需要多条指令才能在哈希表中查找属性的内存位置。这使得Javascript中的属性查找比其他语言慢得多。由于字典表是一种非常低效的查找属性内存位置的方式,V8使用了一种完全不同的方法来改进它,即隐藏类。其实不管隐藏类在运行时的作用有什么区别,它都非常类似于Java中的固定对象结构。在阅读以下内容之前,请先明确两点。首先,V8将一个隐藏类与每个对象相关联。其次,隐藏类的目的是为了优化属性的访问速度。现在让我们进入正题。函数点(x,y){this.x=x;this.y=y;}constobj=newPoint(1,2);一旦声明了一个新方法,Javascript就会创建一个隐藏类C0。此时尚未声明任何属性,因此C0现在为空。一旦执行了第一条语句“this.x=x”,V8将基于C0创建第二个隐藏类C1。C1记录在内存中可以找到属性x的位置。在这个例子中,x存储在偏移量0处,这意味着内存中的一个对象对象可以看作是一个连续的空间。而这个空间的第一个偏移量代表属性x。同时,V8会用一个“类偏移量”操作来更新C0,这意味着属性x已经添加到目标对象中。之后目标对象对应的隐藏类指针就会指向C1。每当将新属性添加到目标对象时,对象的旧隐藏类就会转换为新的隐藏类。隐藏类的重要之处在于,通过相同创建过程创建的对象可以共享隐藏类。如果两个对象共享一个隐藏类,并且同时给两个对象添加相同的属性,那么这种转换将保证转换后得到相同的隐藏类,从而优化代码。执行“this.y=y”时会重复上述操作。会新建一个名为C2的隐藏类,然后对C1进行类转换,表示目标对象已经添加了属性y,最后隐藏类指向C2。这样,目标对象的隐藏类就更新为C2。注意:隐藏类的变换取决于目标对象属性的添加顺序。注意下面的代码:1functionPoint(x,y){2this.x=x;3this.y=y;4}57varobj1=newPoint(1,2);8varobj2=newPoint(3,4);910obj1.a=5;11obj1.b=10;1213obj2.b=10;14obj2.a=5;直到第九行,obj1和obj2共享同一个隐藏类。然而,当属性a和b以相反的顺序添加到两个对象时,这会导致两个不同的隐藏类,最后两个对象具有不同的转换路径。说到这里,可能有读者会认为两个对象有两个不同的隐藏类并不是什么大问题。只要正确的偏移量存储在隐藏类中,访??问属性应该和共享相同的隐藏类一样快。要理解为什么这个想法是错误的,我们需要引入另一个V8优化技术,内联缓存。内联缓存(InlineCaching)V8用来优化动态类型语言性能的另一种技术叫做“内联缓存”。如果想详细了解内联缓存,可以参考这里,但简单来说,内联缓存依赖于一个观察到的现象,即重复调用方法有很大概率使用同类型的参数。那么它究竟是如何工作的呢?V8会维护一个缓存,记录最近一段时间调用方法时传入的参数类型,然后根据获取到的信息预测以后调用时传入的参数类型。V8引擎一旦正确预测出参数的类型,就会让引擎跳过解析如何访问类属性的过程,直接使用之前缓存的信息直接获取隐藏类,访问对象属性。那么为什么隐藏类和内联缓存的概念如此密切相关呢?每当使用特定对象调用方法时,V8引擎都会查找该对象的隐藏类,以获得后续访问特定属性的偏移量。两次成功调用具有相同隐藏类参数的同一个方法后,V8引擎将省略隐藏类查找过程,直接添加属性的偏移量。对于后续的调用,引擎会假设隐藏类不会发生变化,直接使用上次查找的缓存偏移量来访问内存,这样会大大提高访问速度。内联缓存是对象共享隐藏类的一个重要原因。如果创建两个类型相同但隐藏类不同的对象,引擎将无法针对内联缓存优化它们,因为不同的隐藏类代表不同的属性偏移量。与优化相关的建议保证对象属性以相同顺序实例化,这保证它们共享相同的隐藏类。在对象实例化后向对象添加属性将强制更改隐藏类,这将减慢对同样已经内联缓存的方法的访问。因此,请尽量确保所有属性声明都在构造函数内进行。反复执行相同方法的代码比总是执行不同方法的代码要快。
