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