0%

探索 JS 引擎工作原理

javascript 核心概念

javascript 有很多奇怪的知識點,但看來看去總覺得十分分散,沒有連接的感覺,因此整理一篇結合一下 js 原理

一切都要從 js 如何運行的開始講起

javascript 引擎在執行一段代碼前,會先經過編譯階段,此階段會將 javascript 語法解析成 AST,再編譯成 Byte code 並逐行解釋執行,若有重複執行的 function,會再編譯成優化的 machine code 存起來,之後執行就可以直接使用提升效率

V8 引擎內部使用多個線程:

  • 主線程:獲取代碼進行編譯,然後執行
  • 單獨的編譯線程: 讓主線程在執行代碼優化時繼續執行 js
  • Profiler 線程: 告訴 runtime 我們在哪些方法上花費了很多時間,以便 turbofan 優化
  • 其他線程: 垃圾收集器清除

接下來詳細介紹執行代碼的步驟

create phase

JS 引擎在進入一段可執行的代碼時,需要完成以下三個初始化工作:

1. 全域初始化

js 引擎會建立一個 global Object(GO),屬性到任何 scope 都可以訪問,裡面包含 DATE, Object, Array, String屬性,而其中有個 window 屬性指向自己

1
2
3
4
5
6
7
8
varglobalObject = {
Math:{},
String:{},
Date:{},
document:{},
...
window:this
}

2. 構建 Execution Context Stack

同時創建 global Execution Context(GEC),並將 GEC 推進 Execution Context Stack 中

1
2
3
4
5
6
7
// 偽代碼
var ECStack = []; //定義一个執行環境 stack

var EC = {}; //創建 EC

ECStack.push(EC); //推入執行環境,進入 EC
ECStack.pop(EC); //function return後,删除 EC


可以看到 Global EC 永遠在最底層,直到關閉頁面時才會銷毀

3. 解析 global code

scan global code 將全域變數透過 parser 轉為 AST,並將定義的變數、函數加入 global object(GO) 中

GO 包含了全域對象所有的屬性、全域變數、函數

1
2
3
4
5
6
7
8
9
10
ECStack = [
EC(G) = {
VO(G):{
... //包含 global object 原有的屬性, ex: Math, Date 等等
x = 1; // 定義變數
A = function(){...}; //定義 function,此時 lazy-parse,不產生 function 的 AST
A[[scope]] = this; //定義 A 的 scope,根據 code site 由自身 VO + 外部 VO(包含 GO)
}
}
];

最後將 ast 轉為 byteCode,接著就可以執行 main script 進入 execution phase 了

這種先 parse 成 AST 加入 VO 中,再進行 execution phase 的行為,人稱「hoisting」

另外由於 var 聲明的變量不支持 block 級作用域(if, for, while),因此 block 中的 var 會直接加入 VO 中

execution phase

進入執行階段,js 解釋器 Ignition 會逐行解釋 byte code 成 machine code 並執行,此時語句又分

  • LHS: 賦值,變數在左
  • RHS: 查找值,變數在右

若要執行 function 則會

1. 進入 creation phase 並創建 Functional execution context (FEC)

建立一個 execution context,壓入 EC stack 中,並 fully parse AST,建立 activation object(AO)(內含參數及 local variable)並定義 scope chain 及 thisValue

  • VO: 放變數、函式定義、參數等等,是一個 abstract concept,在 GEC 中稱 GO,在 FEC 中稱 AO
  • scope chain: 由 VO 及 AO 構成的鍊,查找不到屬性時會一層層去上層找
  • this: 在進入 EC 時根據呼叫方式決定 this 值,之後 execution phase 就無法改變 this 值
    • Default binding | Direct invocation: 指向 Window
    • Implicit binding | Method invocation: 物件中的 function
    • Explicit binding | Indirect invocation: apply, bind, call
    • New binding | Constructor invocation: new operator construct 出來的

在 strict mode 中,如果 this 沒有在進入 execution context 時被設置,就會維持是 undefined

把 function 宣告放到 VO 裡,如果已經有同名的就覆蓋掉
把變數宣告放到 VO 裡,如果已經有同名的則忽略

Ignition 將 AST 轉為 byteCode 後,進入 execution phase

2. 在 FEC 中進行 execution phase

Ignition 會逐行解釋 byte code 成 machine code 並執行,此時語句又分

  • LHS: 賦值,變數在左
  • RHS: 查找值,變數在右

函數執行完成後,垃圾回收 AO,若 AO 仍在被引用(reference) 就不會被釋放,利用這個特點可以生成閉包,即使 outerFunction 執行完畢彈出 ec stack 後,也可以使用其變量

若執行 code 過程中要存取 this,會直接從 execution context 取得而不用查找 scope-chain

scope chain

scope chain 是根據 code site 由 VO 所構成

另外 let 跟 const 實現 block scope 的方法就是將 {} 內的區域變數在 scope chain 中推入一層屬於 block 的 scope(新增在原本 function scope 之上)

1
2
3
4
5
6
7
8
9
var g = 'g';
function fa() {
var a = 'a';
function fb() {
var b = 'b';
}
fb();
}
fa();

this的指向是跟著 execution context 的,而讀取變數是跟著 scope 的

參考資料

ECMA-262

探索JS引擎工作原理

How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code

我知道你懂 hoisting,可是你了解到多深?

所有的函式都是閉包:談 JS 中的作用域與 Closure

编译器和解析器:V8如何执行一段JavaScript代码的

ignition & turboFan

When does V8 starts compiling and executing the code in relation to the event loop stack?

JavaScript深入浅出第4课:V8引擎是如何工作的?

精读《V8 引擎 Lazy Parsing》

解析JavaScript — lazy 是否比 eager更好?

Understanding V8’s Bytecode

Execution context and activation object in details

V8 Hidden class