在這篇文章中,將會介紹 JS 是如何透過 JavaScript Engine 做轉換,變成電腦讀的懂並可以執行的機器語言以及介紹 JS 引擎內部幾個優化其執行速度的機制。
因為 JS 是一種高階的程式語言,它不能直接給電腦執行,所以需要透過 JS 引擎將 JS 轉換成更為底層的機器語言交給電腦去執行。
那麼 JS 引擎存在於哪裡呢?答案是瀏覽器內部,不過因為瀏覽器有很多種,所以每家瀏覽器也推出了自己的 JS 引擎,例如 Google's Chrome 使用 V8、Safari 使用 JavaScriptCore、 Firefox 使用 SpiderMonkey。
而目前大部分 JS 引擎使用了合併 Interpreter & Compiler 兩者優點的 JIT Compiler(Just-in-time compiler),更加提升了 JS 執行速度。
在初步知道 JS 引擎後,我們來了解它的運作過程,因為每個 JS 引擎實際上轉換程式碼的步驟並不一定完全相同,故以下步驟以 V8 為舉例。
首先 JS 引擎的某個模組 "Parser" 會將 JS 語法做解析,在 AST Explorer 網站上,可以看到網站預設的範例,如左邊紅框的 tips 變數,經過解析後會變成右邊紅框的樣子,而右邊整個的程式內容就稱為 AST。
V8 內部的直譯器會一行行地將 AST 轉換成 ByteCode,而 ByteCode 部分需要優化的 ByteCode(如重複執行的程式碼)會交給 Compiler 做優化。
如果想看 ByteCode 長什麼樣子,可以選一個 JS 檔並輸入以下指令執行:
node --print-bytecode 檔名.js
補充: node.js 執行也是使用 V8 引擎
在 V8 內部,多次且常執行的函式可能會進入到這個步驟,這些將被優化的 ByteCode 會透過 Compiler 轉換成 Machine Code。
如果想看 Compiler 編譯完的程式碼,可以選一個 JS 檔並輸入以下指令執行:node --print-code --print-opt-code 檔名.js
JS Engine 為了提升執行速度,在內部設計了一些機制,而以下提到的兩個機制有借鑑到了靜態語言的特性。
在靜態語言中,建立物件之前會先建立好 class 並用 new 關鍵字去產生物件,而在物件建立好後,其每個屬性都會儲存一份記憶體偏移量,因此可以直接透過"偏移量"去取得屬性值。
而在 JS 引擎中,JS 物件的各個屬性是以下圖的樣子在記憶體做儲存的,分為 Named properties & Array-indexed properties 兩個像陣列或是字典的資料結構去儲存,再根據物件的使用情境去決定要用哪邊的方式去取屬性值,讓存取和修改屬性更有效率。
圖片改編自 V8 官網的文章
在 V8 執行過程中,會假設 JS 的物件是靜態的,也就是物件建立好後不會去增加新屬性,也不會刪除屬性。
Hidden classes 會記錄物件的一些資訊,例如上圖物件在記憶體的樣子、物件原型的參考、物件屬性數量等。
每當物件新增或是刪除屬性時,會改變 Hidden classes,而如果兩個物件的屬性和值都完全相同時,會共用同一個 Hidden classes,減少記憶體使用空間。
由上面的 Hidden classes 特性可以知道幾個讓效能更好的小技巧(即使我們感覺不出來XD):
若讀者有興趣進一步了解 Hidden classes 的運作過程,可以參考這篇文章。
Inline caching 的目的是加速運算,在 V8 引擎內部如果多次用到同一個 Hidden class 並取得相同的屬性值,就會透過 Inline caching 機制進行暫存,當下次又使用到相同的物件屬性值時,將會直接取得屬性值,跳過了從Hidden classes 查找屬性的過程。
ex:
function findUser(user) {
return `found ${user.firstName} ${user.lastName}`;
}
const userData = {
firstName: 'John',
lastName: 'Lin',
}
findUser(userData); // `found John Lin
findUser(userData); // 底層運作會直接傳 `found John Lin,不查找 useData 的屬性值
findUser(userData); // 直接傳 `found John Lin,不查找 useData 的屬性值
實際上 JS 引擎的任務不只是要編譯程式碼,它還要負責執行程式碼、分配記憶體以及垃圾回收。因此在這個段落要來介紹 JS 引擎內部的兩個東西,Call Stack & Memory Heap 以及一些相關名詞。
Call Stack 可以用來記錄目前程式執行到哪裡,在 JS 執行的過程中,會將要執行的程式碼依照執行的先後順序加入到 Call Stack 裡面,並且它是一個堆疊的資料結構。
JS 是一個 Single Threaded 單線程的程式語言,也就是只有一個 Call stack
以下為範例:
function addTwoNums(x, y) {
return x + y;
}
function showSum(x) {
const sum = addTwoNums(x, x);
console.log(sum);
}
showSum(5);
另外在 Chrome devtool 也可以在 Source 看到 Call Stack,有興趣的讀者可以自行打開來看。
Call Stack 是有限制的,如果在 Stack 內執行的任務不斷的堆積而沒有 pop 出去,就會造成 Stack Overflow。
Stack Overflow 範例,一直呼叫 inception 函式。
function inception() {
inception();
}
inception();
用來配置記憶體和儲存資料,可以想像 Memory Heap 內有很多個格子,然後把一筆筆資料存入格子內,格子上有 address,透過 address 就可取出格子內的資料。
當建立變數、函式時都會配置記憶體去儲存該變數的資料,而在 V8 引擎中,有個 garbage collector,它會從記憶體內將用不到的資料回收。
不過即使 JS 這個語言會自動幫你做 GC,雖然可以不用撰寫釋放記憶體的程式碼,但 JS 沒有主動釋放記憶體的 API,若程式沒有寫好可能會有 Memory Leaks 的問題。
若存在 Memory Heap 的資料已經不會用到了,但卻一直沒有被 GC,就會導致 Memory Leaks。
關於 Garbage Collection & Memory Leaks,礙於篇幅關係,在明天的文章將會有更詳細的介紹。
How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code
浏览器是如何工作的:Chrome V8让你更懂JavaScript
感謝大大分享關於 js engine 的機制,讓我能夠理解底層的運作原理。受益良多!
謝謝支持!