Hidden Classes
Javascript,眾所周知是一門動態類型語言,也就是說當一個對象被實例化之后,我們仍然可以隨意的添加或者刪除它的屬性。例如,下面的代碼中,我們實例化了一個car
,包含有make
和model
兩個屬性,同時,當car
被實例化了之后我們仍可以將其他屬性year
賦予它。
```javascript
var car = function(make,model) {
this.make = make;
this.model = model;
}
var myCar = new car(honda,accord);
myCar.year = 2005;
大多數的JavaScript解釋器通過類字典(dictionary-like,基于hash的)型的對象來存儲對象中的屬性。這樣,相對于非動態類型語言(比如Java),這種結構的數據類型讓我們在獲取對象屬性時會消耗更多的時間。在Java中,所有的對象屬性會在編譯之前就被一個固定的對象層所決定,同時在運行時不可修改。因此,這些屬性的值(或者指向屬性的指針)可以一個接一個的連續得存儲在內存中,每一個屬性都有一個固定的偏移量。偏移量可以簡單的通過屬性的類型決定,顯而易見,由于JavaScript是一門動態類型語言,所以無法在運行時確定偏移量。
在非動態類型語言中,例如Java只需要一條指令就可以確定一個屬性在內存中的位置,然而JavaScript則需要多條指令來從Hash Table中獲得位置。這就導致在JavaScript中屬性查找通常會比其他語言慢很多。
鑒于基于字典的存儲方式非常的低效,V8采用了一個完全不同的方式:Hidden Class。除了JavaScript是在運行時完成一系列操作之外,Hidden Class的工作原理和Java中對待對象的處理方式很類似。在閱讀剩下的文章時,請記住V8會給每一個對象都賦予一個Hidden Class,Hidden Class的終極目的就是優化屬性的查詢時間。現在我們來具體看看。
function Point(x,y) {
this.x = x;
this.y = y;
}
var obj = new Point(1,2);
一旦一個新的function被聲明,JavaScript就會緊接著創建一個Hidden Class C0
。

由于暫時還沒有屬性,所以C0
是空的。
當執行到this.x = x
時,V8會創建第二個Hidden Class叫做C1
,C1
是基于C0
的。C1
描述了如何找到屬性x
,也就是x
在內存中的位置。在這個例子中,簡化為將x
存在偏移值為0的位置,這表明當我們在內存中查看Point
對象時,第一個偏移所在的位置會保存屬性x
。V8同時會用class transition
將C0
更新,簡單來說就是Hidden Class現在切換為C1
了。換句話說,C1
就變成了Point
對象的Hidden Class。

每當對象有新屬性加入時,舊的Hidden Class就會通過一個過渡路徑(transition path)更新為新的Hidden Class。這種過渡非常重要,因為這保證了用相同方式創建的兩個對象可以共享同一個Hidden Class。如果有兩個對象共享同一個Hidden Class,同時又有一個屬性同時添加給了這兩個對象,則這種過渡操作保證了新的Hidden Class仍然是這兩個對象的Hidden Class。
當this.y = y
執行時,重復之前的操作。一個新的叫做C2
的Hidden Class被創建,一個過渡會添加給C1
,同時Hidden Class轉換為C2
,Point
對象的Hidden Class現在就被更新為了C2
。

注意:Hidden Class的過渡取決于屬性被添加的順序。
function Point(x,y) {
this.x = x;
this.y = y;
}
var obj1 = new Point(1,2);
var obj2 = new Point(3,4);
obj1.a = 5;
obj1.b = 10;
obj2.b = 10;
obj2.a = 5;
到var obj2 = new Point(3,4);
為止,obj1和obj2共享同一個Hidden Class。但是,由于屬性a
和b
在之后被添加的順序不同,導致了Hidden Class過渡時走向和不同的路徑。
通過上述的例子,也許你直觀上會覺得obj1和obj2擁有不同的Hidden Class并沒有什么關系。因為每一個Hidden Class都保存著合適的偏移量,那么不管obj1和obj2有沒有共享同一個Hidden Class,他們獲取屬性的速度應該是一樣的。為了理解為什么這樣的想法是錯的,讓我們來學習一個V8中的優化技術,叫做內聯緩存(inline caching)。
內聯緩存
V8利用了一個在動態類型語言中常用的優化技巧,內聯緩存。簡單來說,內聯緩存依賴于同類型對象的同一方法的重復調用。
那么這是怎么實現的呢?V8維護了一個緩存——最近被調用的函數中,被作為參數傳遞的對象類型,然后利用這些信息并假設這種對象類型在將來仍然會被作為參數傳遞。如果V8能正確的做出這種預測,那么它就能繞過尋找對象屬性的過程,直接從對象的Hidden Class中尋找之前存好的信息。
好的,那么Hidden Class的思想是這么和內斂緩存相結合起來的呢?當一個特定的對象的方法被調用時,V8引擎需要利用對象的Hidden Class來決定用哪個偏移量來尋找屬性。當針對同一個Hidden Class的方法被成功調用兩次,V8就會跳過Hidden Class的屬性尋找,直接將屬性的偏移量綁定到對象的指針上。當該方法再次被調用時,V8引擎假定Hidden Class沒有被修改,我們可以直接用上次在Hidden Class中查找到的屬性偏移量來去內存中尋找具體的值。這大大提高了執行的速度。
這就是為什么屬性的添加順序顯得很重要了。如果屬性的添加順序不同,兩個對象的Hidden Class就不同,也就無法使用內斂緩存優化了。
當然,鑒于JavaScript仍然是門動態類型語言,所以總會有判斷失誤的時候。這時候就需要返回傳統的方法,利用Hidden Class來獲取屬性的偏移量了。
一些優化技巧
- 實例化對象時永遠保存屬性的順序相同。
- 給對象增加新的屬性時,會導致Hidden Class更新,同時針對舊Hidden Class的優化失效,導致速度變慢。因此,盡量在構造函數中確定好對象的屬性。
- 運行同一個方法多次會比運行多個方法一次要快的多(參照內聯緩存)。