從變數提升到 V8 預編譯,徹底搞懂 JS 執行機制

引言

--

寫 JS 誰沒被變數提升坑過?明明變數寫在後面,前面卻印出 undefined;函式定義在末尾,開頭就能直接呼叫。很多人背了「宣告會提升」的口訣,卻以為是程式碼被偷偷搬到了頂部 —— 大錯特錯!這根本不是什麼玄學 bug,而是 V8 引擎在程式碼執行前悄悄搞的預編譯小動作。

1.變數提升現象

那什麼是變數提升呢?接下來用一段程式碼告訴你:

js 体验AI代码助手 代码解读复制代码    console.log(a);
    var a=2;

輸出結果為:

image.png

你看,我們明明先印出 a、後定義 a,輸出卻不是預期的報錯,而是 undefined。這就是前端入門必踩的「變數宣告提升」坑。它的本質是:V8 引擎在程式碼執行前,會先偷偷把變數 a 的宣告提到目前作用域的最前面,賦值操作卻原封不動留在原地。所以這段程式碼的實際執行順序,相當於引擎幫你偷偷改成了這樣:
image.png

2.什麼是 V8 引擎的預編譯?

  • 預編譯發生在程式碼執行之前,但並不是「一次性編譯完所有程式碼」,函式的預編譯是在「函式被呼叫時」才觸發,全域預編譯在程式碼載入時觸發。
  • 預編譯只處理「宣告」(變數宣告 var、函式宣告 function),不處理「賦值操作」。這也是為什麼變數提升後,值是 undefined 的原因。
  • let、const 沒有變數提升,嚴格來說是「暫時性死區」,本質是 V8 引擎對 let、const 的預編譯處理和 var 不同,後續會在誤區中補充。

3.預編譯的兩個場景 GO 全域/AO 函式

3.1 AO 函式

函式的預編譯,只有在函式被呼叫時才會觸發,核心是建立 AO 物件(執行上下文物件),用一段程式碼解釋:

js 体验AI代码助手 代码解读复制代码function foo(a, b) {
  console.log(b);
  c = 0
  var c
  a = 3
  b = 2
  console.log(b);
  function b() {}
  console.log(b);
}
foo(1)

步驟如下:

  1. 建立一個執行上下文物件 AO:{},初始值為空物件。
  2. 去函式體內找形參和變數宣告,將形參和變數名作為屬性名,加入到 AO 中,值為 undefined。
  • 形參:a、b → AO 新增屬性 a: undefined,b: undefined
  • 變數宣告:var c : undefined
  • 此時 AO:{a: undefined, b: undefined, c: undefined}
  1. 將形參和實參統一。
  • 函式呼叫時實參是 1,對應形參 a → a = 1
  • 實參只有一個,形參 b 沒有對應實參,仍為 undefined
  • 此時 AO:{a: 1, b: undefined, c: undefined}
  1. 在函式體內找到函式宣告,將函式名作為 AO 中的屬性名,函式體作為屬性值。
  • 函式宣告:function b() {} → AO 新增屬性 b: function b() {}
  • 此時 AO:{a: 1, b: function b() {}, c: undefined }

最後輸出結果為:

image.png

如果還是覺得抽象,我給你打個最通俗的比方:我們把 V8 引擎比作一家公司的大老闆,他脾氣很倔,只認最終的執行指令,拿到手就直接幹,絕不幫你整理亂七八糟的材料。而預編譯,就是老闆身邊那個能力超強的全能女秘書。

當你把一整段 JS 程式碼(也就是一堆待處理的工作任務)丟給公司時,絕不會直接堆到老闆桌上。秘書會第一時間把所有任務全部過一遍,雷打不動做兩件核心工作:

  1. 先把所有立項申請單獨挑出來:也就是所有的 var 變數宣告和 function 函式宣告,提前拿去給老闆簽字審批。老闆簽完字,就代表這個專案(變數 / 函式)在公司系統裡正式存在了,只是還沒分配資源開始幹活(預設值為 undefined)。
  2. 再把剩下的具體執行任務按原順序整理好:也就是賦值、運算、函式呼叫、列印這些真正幹活的程式碼,等所有立項全部審批完成後,再按順序交給老闆執行。

3.2 GO 全域

全域預編譯,在程式碼載入完成後、執行之前觸發,核心是建立 GO 物件(全域執行上下文物件),同樣結合案例:

js 体验AI代码助手 代码解读复制代码function a () {}
console.log(a);
var a = 1
var b = 2

步驟如下

  1. 建立全域執行上下文物件 GO:{}
  2. 去全域體內找變數宣告,將變數名作為屬性名,加入到 GO 中,值為 undefined。
  • 變數宣告:var a,b → GO 新增屬性 a: undefined,b: undefined;
  • 此時 GO:{a: undefined, b: undefined}
  1. 將函式宣告作為屬性名,函式體作為屬性值,加入到 GO 中。
  • 函式宣告:function a() {} → GO 屬性中 a 重新被賦值:function a() {}
  • 此時 GO:{a: function a(){}, b: undefined}

最後輸出:

image.png

4.總結

到這裡,我們就徹底搞懂了 JS 變數提升和預編譯的底層邏輯。所有看似反直覺的變數提升現象,本質上都不是程式碼被物理移動到了作用域頂部,而是 V8 引擎在程式碼執行前,透過預編譯階段提前完成了所有宣告的記憶體初始化。

  • 預編譯的觸發時機:全域預編譯在程式碼載入完成後觸發,函式預編譯只在函式被呼叫的瞬間觸發,函式執行完畢後對應的 AO 物件會立即銷毀。
  • 預編譯的核心工作:只處理 var 變數宣告和 function 函式宣告,完全不處理賦值、運算、列印等執行邏輯,這也是為什麼變數提升後預設值是 undefined 的根本原因。
  • 兩個核心物件的執行規則:
  1. 全域預編譯(GO):建立全域執行上下文 → 收集所有 var 變數宣告並賦值為 undefined → 收集所有函式宣告並賦值為函式體
  2. 函式預編譯(AO):建立函式執行上下文 → 收集形參和 var 變數宣告並賦值為 undefined → 形參實參統一 → 收集內部函式宣告並賦值為函式體
  • 黃金優先級規則:函式宣告提升優先級 > 變數宣告提升優先級,同名情況下函式宣告會先占據記憶體,變數宣告不會覆蓋既有的函式宣告,只有後續的賦值操作才會覆蓋。

原文出處:https://juejin.cn/post/7639644871033241651


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝17   💬11   ❤️1
572
🥈
alicec
📝1   ❤️2
79
🥉
我愛JS
💬2  
7
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登