您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關JavaScript引擎的基本原理是什么,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
這一切都要從你寫的 JavaScript 代碼開始。JavaScript 引擎解析源代碼并將其轉換為抽象語法樹(AST)。基于 AST,解釋器便可以開始工作并生成字節碼。就在此時,引擎開始真正地運行 JavaScript 代碼。為了讓它運行得更快,字節碼能與分析數據一起發送到優化編譯器。優化編譯器基于現有的分析數據做出某些特定的假設,然后生成高度優化的機器碼。
如果某個時刻某一個假設被證明是不正確的,那么優化編譯器將取消優化并返回到解釋器階段。
現在,讓我們來看實際執行 JavaScript 代碼的這部分流程,即代碼被解釋和優化的部分,并討論其在主要的 JavaScript 引擎之間存在的一些差異。
一般來說,JavaSciript 引擎都有一個包含解釋器和優化編譯器的處理流程。其中,解釋器可以快速生成未優化的字節碼,而優化編譯器會耗費更長的時間,但最終可生成高度優化的機器碼。這個通用流程和 Chrome 和 Node.js 中使用的 Javascript 引擎, V8 的工作流程幾乎一致:V8 中的解釋器稱為 Ignition,負責生成和執行字節碼。當它運行字節碼時,它收集分析數據,這些數據可用于后面加快代碼的執行速度。當一個函數變為 hot 時,例如當它經常運行時,生成的字節碼和分析數據將傳遞給我們的優化編譯器 Turbofan,以根據分析數據生成高度優化的機器代碼。Mozilla 在 Firefox 和 Spidernode 中使用的 JavaScript 引擎 SpiderMonkey ,則不太一樣。它們有兩個優化編譯器,而不是一個。解釋器先通過 Baseline 編譯器,生成一些優化的代碼。然后,結合運行代碼時收集的分析數據,IonMonkey 編譯器可以生成更高程度優化的代碼。如果嘗試優化失敗,IonMonkey 將返回到 Baseline 階段的代碼。
Chakra,在 Edge 中使用的 Microsoft 的 JavaScript 引擎,非常相似的,也有2個優化編譯器。解釋器優化代碼到 SimpleJIT(JIT 代表 Just-In-Time 編譯器,即時編譯器),SimpleJIT 會生成稍微優化的代碼。而 FullJIT 結合分析數據,可以生成更為優化的代碼。JavaScriptCore(縮寫為 JSC),在 Safari 和 React Native 中使用的 Apple 的 JavaScript 引擎,它通過三種不同的優化編譯器將其發揮到極致。低層解釋器 LLInt 優化代碼到 Baseline 編譯器中,然后優化代碼到 DFG(Data Flow Graph)編譯器中,DFG(Data Flow Graph)編譯器又可以將優化后的代碼傳到 FTL(Faster Than Light)編譯器中。
為什么有些引擎有更多的優化編譯器?這是權衡利弊的結果。解釋器可以快速生成字節碼,但字節碼通常效率不高。另一方面,優化編譯器需要更長的時間,但最終會產生更高效的機器代碼。在快速讓代碼運行(解釋器)或花費更多時間,但最終以最佳性能運行代碼(優化編譯器)之間需要權衡。一些引擎選擇添加具有不同時間/效率特性的多個優化編譯器,允許在額外的復雜性的代價下對這些權衡進行更細粒度的控制。另一個需要權衡的方面與內存使用有關,后續會有專門的文章詳細介紹。
我們剛剛強調了每個 JavaScript 引擎中解釋器和優化編譯器流程中的主要差異。除了這些差異之外,在高層上,所有 JavaScript 引擎都有相同的架構:那就是有一個解析器和某種解釋器/編譯器流程。
讓我們通過放大一些方面的實現來看看 JavaScript 引擎還有什么共同點。
例如,JavaScript 引擎如何實現 JavaScript 對象模型,以及它們使用哪些技巧來加速訪問 JavaScript 對象的屬性?事實證明,所有主要引擎在這一點上的實現都很相似。
ECMAScript 規范基本上將所有對象定義為由字符串鍵值映射到 property 屬性的字典。
除了 [[Value]] 本身,規范還定義了這些屬性:
[[雙方括號]] 的符號表示看上去有些特別,但這正是規范定義不能直接暴露給 JavaScript 的屬性的表示方法。在 JavaScript 中你仍然可以通過 Object.getOwnPropertyDescriptor API 獲得指定對象的屬性值:
const object = { foo: 42 };Object.getOwnPropertyDescriptor(object, 'foo');// → { value: 42, writable: true, enumerable: true, configurable: true }復制代碼
這就是 JavaScript 定義對象的方式,那么數組呢?
你可以把數組看成是一個特殊的對象,其中的一個區別就是數組會對數組索引進行特殊的處理。這里的數組索引是 ECMAScript 規范中的一個特殊術語。在 JavaScript 中限制數組最多有 232?1個元素,數組索引是在該范圍內的任何有效索引,即 0 到 232?2 的任何整數。
另一個區別是數組還有一個特殊的 length 屬性。
const array = ['a', 'b']; array.length; // → 2array[2] = 'c'; array.length; // → 3復制代碼
在該例中,數組被創建時 length 為 2。當我們給索引為 2 的位置分配另一個元素時,length 自動更新了。
JavaScript 定義數組的方式和對象類似。例如,所有的鍵值, 包括數組的索引, 都明確地表示為字符串。數組中的第一個元素,就是存儲在鍵值 '0' 下。“length” 屬性是另一個不可枚舉且不可配置的屬性。 當一個元素被添加到數組中時, JavaScript 會自動更新 “length“ 屬性的 [[value]] 屬性。
知道了對象在 JavaScript 中是如何定義的, 那么就讓我們來深入地了解一下 JavaScript 引擎是如何高效地使用對象的。 總體來說,訪問屬性是至今為止 JavaScript 程序中最常見的操作。因此,JavaScript 引擎是否能快速地訪問屬性是至關重要的。
在 JavaScript 程序中,多個對象有相同的鍵值屬性是非常常見的。可以說,這些對象有相同的 shape。
const object1 = { x: 1, y: 2 };const object2 = { x: 3, y: 4 };// object1 and object2 have the same shape.復制代碼
訪問擁有相同 shape 的對象的相同屬性也是非常常見的:
function logX(object) { console.log(object.x); }const object1 = { x: 1, y: 2 };const object2 = { x: 3, y: 4 }; logX(object1); logX(object2);復制代碼
考慮到這一點,JavaScript 引擎可以基于對象的 shape 來優化對象的屬性訪問。下面我們就來介紹其原理。
假設我們有一個具有屬性 x 和 y 的對象,它使用我們前面討論過的字典數據結構:它包含字符串形式的鍵,這些鍵指向它們各自的屬性值。
如果你訪問某個屬性,例如 object.y,JavaScript 引擎會在 JSObject 中查找鍵值 'y',然后加載相應的屬性值,最后返回 [[Value]]。
但這些屬性值存儲在內存中的什么位置呢?我們是否應該將它們作為 JSObject 的一部分進行存儲?假設我們稍后會遇到更多同 shape 的對象,那么在 JSObject 自身存儲包含屬性名和屬性值的完整字典便是一種浪費,因為對于具有相同 shape 的所有對象,屬性名都是重復的。 這是大量的重復和不必要的內存使用。 作為一種優化,引擎將對象的 Shape 分開存儲。shape 包含除了 [[Value]] 以外所有屬性名和屬性。另外,shape 還包含了 JSObject 內部值的偏移量,以便 JavaScript 引擎知道在哪里查找值。具有相同 shape 的每個 JSObject 都指向該 shape 實例。現在每個 JSObject 只需要存儲對這個對象來說唯一的值。當我們有多個對象時,好處就顯而易見了。不管有多少個對象,只要它們有相同的 shape,我們只需要存儲 shape 和屬性信息一次!
所有的 JavaScript 引擎都使用了 shapes 作為優化,但稱呼各有不同:
本文中,我們將繼續使用術語 shapes.
如果你有一個具有特定 shape 的對象,但你又向它添加了一個屬性,此時會發生什么? JavaScript 引擎是如何找到這個新 shape 的?
const object = {}; object.x = 5; object.y = 6;復制代碼
這些 shapes 在 JavaScript 引擎中形成所謂的轉換鏈(transition chains)。下面是一個例子:
該對象開始沒有任何屬性,因此它指向一個空的 shape。下一個語句為該對象添加一個值為 5 的屬性 "x",所以 JavaScript 引擎轉向一個包含屬性 "x" 的 shape,并在第一個偏移量為 0 處向 JSObject 添加了一個值 5。 下一行添加了一個屬性 'y',引擎便轉向另一個包含 'x' 和 'y' 的 shape,并將值 6 添加到 JSObject(位于偏移量 1 處)。
我們甚至不需要為每個 shape 存儲完整的屬性表。相反,每個shape 只需要知道它引入的新屬性。例如,在本例中,我們不必將有關 “x” 的信息存儲在最后一個 shape 中,因為它可以在更早的鏈上找到。要實現這一點,每個 shape 都會鏈接回其上一個 shape:
如果你在 JavaScript 代碼中寫 o.x,JavaScript 引擎會沿著轉換鏈去查找屬性 "x",直到找到引入屬性 "x" 的 Shape。
但是如果沒有辦法創建一個轉換鏈會怎么樣呢?例如,如果有兩個空對象,并且你為每個對象添加了不同的屬性,該怎么辦?
const object1 = {}; object1.x = 5;const object2 = {}; object2.y = 6;復制代碼
在這種情況下,我們必須進行分支操作,最終我們會得到一個轉換樹而不是轉換鏈。
這里,我們創建了一個空對象 a,然后給它添加了一個屬性 ‘x’。最終,我們得到了一個包含唯一值的 JSObject 和兩個 Shape :空 shape 以及只包含屬性 x 的 shape。
第二個例子也是從一個空對象 b 開始的,但是我們給它添加了一個不同的屬性 ‘y’。最終,我們得到了兩個 shape 鏈,總共 3 個 shape。
這是否意味著我們總是需要從空 shape 開始呢? 不一定。引擎對已含有屬性的對象字面量會進行一些優化。比方說,我們要么從空對象字面量開始添加 x 屬性,要么有一個已經包含屬性 x 的對象字面量:
const object1 = {}; object1.x = 5;const object2 = { x: 6 };復制代碼
在第一個例子中,我們從空 shape 開始,然后轉到包含 x 的shape,這正如我們之前所見那樣。
在 object2 的例子中,直接在一開始就生成含有 x 屬性的對象,而不是生成一個空對象是有意義的。
包含屬性 ‘x’ 的對象字面量從含有 ‘x’ 的 shape 開始,有效地跳過了空 shape。V8 和 SpiderMonkey (至少)正是這么做的。這種優化縮短了轉換鏈并且使從字面量構建對象更加高效。
下面是一個包含屬性 ‘x'、'y' 和 'z' 的 3D 點對象的示例。
const point = {}; point.x = 4; point.y = 5; point.z = 6;復制代碼
正如我們之前所了解的, 這會在內存中創建一個有3個 shape 的對象(不算空 shape 的話)。 當訪問該對象的屬性 ‘x’ 的時候,比如, 你在程序里寫 point.x,javaScript 引擎需要循著鏈接列表尋找:它會從底部的 shape 開始,一層層向上尋找,直到找到頂部包含 ‘x’ 的 shape。
當這樣的操作更頻繁時, 速度會變得非常慢,特別是當對象有很多屬性的時候。尋找屬性的時間復雜度為 O(n), 即和對象上的屬性數量線性相關。為了加快屬性的搜索速度, JavaScript 引擎增加了一種 ShapeTable 的數據結構。這個 ShapeTable 是一個字典,它將屬性鍵映射到描述對應屬性的 shape 上。
現在我們又回到字典查找了我們添加 shape 就是為了對此進行優化!那我們為什么要去糾結 shape 呢? 原因是 shape 啟用了另一種稱為 Inline Caches 的優化。
shapes 背后的主要動機是 Inline Caches 或 ICs 的概念。ICs 是讓 JavaScript 快速運行的關鍵要素!JavaScript 引擎使用 ICs 來存儲查找到對象屬性的位置信息,以減少昂貴的查找次數。
這里有一個函數 getX,該函數接收一個對象并從中加載屬性 x:
function getX(o) { return o.x; }復制代碼
如果我們在 JSC 中運行該函數,它會產生以下字節碼:
第一條 get_by_id 指令從第一個參數(arg1)加載屬性 ‘x’,并將結果存儲到 loc0 中。第二條指令將存儲的內容返回給 loc0。
JSC 還將一個 Inline Cache 嵌入到 get_by_id 指令中,該指令由兩個未初始化的插槽組成。
現在, 我們假設用一個對象 { x: 'a' },來執行 getX 這個函數。正如我們所知,,這個對象有一個包含屬性 ‘x’ 的 shape, 該 shape存儲了屬性 ‘x’ 的偏移量和特性。當你在第一次執行這個函數的時候,get_by_id 指令會查找屬性 ‘x’,然后發現其值存儲在偏移量為 0 的位置。
嵌入到 get_by_id 指令中的 IC 存儲了 shape 和該屬性的偏移量:
對于后續運行,IC 只需要比較 shape,如果 shape 與之前相同,只需從存儲的偏移量加載值。具體來說,如果 JavaScript 引擎看到對象的 shape 是 IC 以前記錄過的,那么它根本不需要接觸屬性信息,相反,可以完全跳過昂貴的屬性信息查找過程。這要比每次都查找屬性快得多。
對于數組,存儲數組索引屬性是很常見的。這些屬性的值稱為數組元素。為每個數組中的每個數組元素存儲屬性特性是非常浪費內存的。相反,默認情況下,數組索引屬性是可寫的、可枚舉的和可配置的,JavaScript 引擎基于這一點將數組元素與其他命名屬性分開存儲。
思考下面的數組:
const array = [ '#jsconfeu', ];復制代碼
引擎存儲了數組長度(1),并指向包含偏移量和 'length' 屬性特性的 shape。
這和我們之前看到的很相似……但是數組的值存到哪里了呢?
每個數組都有一個單獨的元素備份存儲區,包含所有數組索引的屬性值。JavaScript 引擎不必為數組元素存儲任何屬性特性,因為它們通常都是可寫的、可枚舉的和可配置的。
那么,在非通常情況下會怎么樣呢?如果更改了數組元素的屬性特性,該怎么辦?
// Please don’t ever do this!const array = Object.defineProperty( [], '0', { value: 'Oh noes!!1', writable: false, enumerable: false, configurable: false, });復制代碼
上面的代碼片段定義了名為 “0” 的屬性(恰好是數組索引),但將其特性設置為非默認值。
在這種邊緣情況下,JavaScript 引擎將整個元素備份存儲區表示成一個字典,該字典將數組索引映射到屬性特性。
即使只有一個數組元素具有非默認特性,整個數組的備份存儲區也會進入這種緩慢而低效的模式。避免對數組索引使用Object.defineProperty!
關于JavaScript引擎的基本原理是什么就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。