在 V8 引擎中設置原型(prototypes)

在 V8 引擎中設置原型(prototypes)

原型(好比 func.prototype )是用來模擬類的實現。它們一般包含類的全部方法,它們的 __proto__ 就是「父類(superclass)」,它們設置好後就不會修改了。前端

原型在設置時的性能表現對於應用程序的啓動時間相當重要,由於此時一般要創建起整個類的層次結構。android

轉換對象形態(Transitioning object shapes)

對象被編碼的主要方式是將隱藏類(描述)對象(內容)分隔開。當一個對象被實例化,和以前來自同一個構造函數的對象使用相同的初始化隱藏類。當屬性被添加,對象從一個隱藏類切換到另外一個隱藏類,一般是在所謂的「轉換樹(transition tree)」中重複以前的轉換。舉個例子,好比咱們有如下的構造函數:ios

function C() {
  this.a = 1;
  this.b = 2;
}
複製代碼

若是咱們實例化一個對象 var o = new C(),它首先會使用一個沒有任何屬性的初始化隱藏類 M0。當 a 被添加,咱們將從 M0 切換到一個新的隱藏類 M1,M1 描述屬性 a。接着添加 b 的時候,咱們再切換到另外一個新的隱藏類來描述 abgit

若是咱們如今實例化第二個對象 var o2 = new C(),它將重複上面的轉換。從 M0 開始,接着 M1,最後是 M2。ab 被添加完成。github

這樣作有三個重要的好處:後端

  1. 儘管建立第一個對象的開銷是很大的,而且要求咱們建立全部隱藏的類和轉換,可是建立後續對象是很是快的。
  2. 結果對象比完整的字典要小。咱們只須要在對象中存儲值,而不須要存儲關於屬性的信息(好比名稱)。
  3. 咱們如今在內聯緩存(inline cache)和優化代碼時有一個對象形態可使用,之後訪問相似形態的對象就能夠在同一位置找,方便快捷。

這樣有利於頻繁建立類似形態的對象。一樣的事情也發生在對象字面量中:{a:1, b:2} 內部也會有隱藏類 M0,M1 和 M2。緩存

網上有不少相關知識講解,你們能夠去看看 Lars Bak 的視頻:bash

YouTube 視頻見:V8: an open source JavaScript engine函數

原型(Prototypes)就像特別的雪花

不一樣於常規構造函數實例化對象,原型是典型的不與其餘對象分享形態的對象。這會帶來三點變化:post

  1. 一般來說,沒有對象能從緩存的轉換(cached transitions)中受益,並且設置轉換樹(transition tree)的開銷也是沒有必要的。
  2. 建立全部轉換隱藏類的內存開銷是很大的。事實上,在改變這個以前,咱們一般會看到爲了一個簡單的原型就要用上一大堆的隱藏類。
  3. 從一個原型中加載實際上並不像在原型鏈中使用那麼常見。若是咱們經過原型鏈從一個原型對象中加載,咱們將不會分發原型的隱藏類,以及須要用不一樣的方法檢查它是否有效。

爲了優化原型,V8 對其形態的跟蹤不一樣於常規的轉換對象,咱們不須要跟蹤轉換樹(transition tree),而是將隱藏類調整爲原型對象,讓它保持高性能。舉個例子,好比執行 delete object.property 會拖慢對象的性能,但若是是原型就不會出現這種狀況。由於咱們老是會保持它們的可緩存性(有些問題咱們還在解決中)。

咱們也改變了原型的設置。原型包含了2個重要的階段:設置使用。原型在設置階段被編譯成字典對象(dictionary objects)。在那個狀態下存儲原型的速度很是快的,並且不須要進入 C++ 的運行時(跨邊界的花銷是很是巨大的)。與建立一個轉換隱藏類來初始化對象相比,這是一個巨大的進步,由於前者必須進入C++ 運行時才行。

任何對原型的直接訪問,或者經過原型鏈訪問原型,都會將它切換成使用狀態,這樣確保了全部訪問今後時開始是快速的。當處於使用狀態,即便你刪除屬性,在刪除以後咱們也會快速的切換回來。

function Foo() {}
// 如今 proto 對象是"設置"模式。
var proto = Foo.prototype;
proto.method1 = function() { ... }
proto.method2 = function() { ... }

var o = new Foo();
// 切換 proto 到"使用"模式。
o.method1();

// 也會切換 proto 到"使用"模式。
proto.method1.call(o);
複製代碼

它是原型嗎?

爲了用上上面說的優化方法,咱們須要知道一個對象是否真的會被做爲原型使用。因爲 JavaScript 的特性,咱們很難在編譯階段分析你的代碼。出於這個緣由,咱們甚至沒有嘗試在對象建立過程當中肯定什麼東西最終會成爲原型(固然,之後可能會發生變化)。一旦咱們看到一個對象賦值給一個原型,咱們將對它進行標記。舉個例子來說:

var o = {x:1};
func.prototype = o;
複製代碼

一開始咱們也不知道 o 用做原型,直到賦值給 func.prototype。我像往常那樣花費巨大的開銷來建立對象。一旦像它那樣被賦值,它就被標記成原型,進入設置階段。當你使用它,就會進入使用階段。

若是你像下面這樣寫,咱們會在屬性添加前就知道 o 是一個原型。因而它將在添加屬性前進入設置階段,後面的代碼執行就會快得多:

var o = {};
func.prototype = o;
o.x = 1;
複製代碼

注意你也能夠這樣使用 var o = func.prototype,由於很顯然 func.prototype 在建立時就知道它是一個原型。

怎樣設置原型(prototypes)?

若是你用下面的方式設置原型,咱們在方法添加以前很容易就知道 func.prototype 就是一個原型:

// 若是默認的 Object.prototype 爲 __proto__,則省略下面這行代碼。
func.prototype = Object.create(…);
func.prototype.method1 = …
func.prototype.method2 = …
複製代碼

雖然已經很不錯了,但事實上咱們不得不爲每一個方法都加載一次 func.prototype。儘管最近咱們正在進一步優化 func.prototype 的加載,但這種加載是沒必要要的,性能和內存的使用將比直接訪問本地變量訪問更糟糕。

簡而言之,理想的原型設置方法以下:

var proto = func.prototype = Object.create(…);
proto.method1 = …
proto.method2 = …
複製代碼

感謝 Benedikt Meurer.


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索