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 | varglobalObject = { |
2. 構建 Execution Context Stack
同時創建 global Execution Context(GEC),並將 GEC 推進 Execution Context Stack 中
1 | // 偽代碼 |
可以看到 Global EC 永遠在最底層,直到關閉頁面時才會銷毀
3. 解析 global code
scan global code 將全域變數透過 parser 轉為 AST,並將定義的變數、函數加入 global object(GO) 中
GO 包含了全域對象所有的屬性、全域變數、函數
1 | ECStack = [ |
最後將 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 | var g = 'g'; |
this的指向是跟著 execution context 的,而讀取變數是跟著 scope 的
參考資料
How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code
When does V8 starts compiling and executing the code in relation to the event loop stack?
解析JavaScript — lazy 是否比 eager更好?