從 JS 引擎原理開始:用 V8 的視角看懂程式碼是怎麼跑起來的實戰指南

身為前端工程師,我們每天寫 JavaScript,但你有沒有想過:瀏覽器拿到你的程式碼之後,到底是「誰」在執行它?為什麼同樣一段迴圈,有時快如閃電、有時慢如蝸牛?為什麼 Promise 的回調總是比 setTimeout 先執行?這些問題的答案,都藏在 JS 引擎(以 V8 為代表)的運作原理裡。今天這篇,我們就掀開引擎蓋,看看你寫的程式碼是怎麼被跑起來的。

一、JS 引擎是什麼?從原始碼到機器碼的四段旅程

JS 引擎(Chrome 的 V8、Firefox 的 SpiderMonkey、Safari 的 JavaScriptCore)本質上是一台「翻譯機 + 執行器」,把你寫的人類語言翻譯成 CPU 聽得懂的機器碼。以 V8 為例,流程大致是:

  • 解析(Parser):把原始碼切成 Token、建立抽象語法樹(AST)。語法寫錯,就是在這一步報錯。
  • 直譯執行(Ignition):V8 的直譯器把 AST 編譯成位元組碼(Bytecode)先跑起來——犧牲一點性能,換取「快速啟動」。
  • 即時編譯(JIT,TurboFan):引擎一邊跑一邊統計哪段程式碼是「熱點(Hot Code)」,把被反覆執行的函式送進優化編譯器,編成高效的機器碼。
  • 反優化(Deopt):如果引擎發現先前的假設被打破(例如函式原本都收數字、突然傳了字串),就退回位元組碼重新來過。

重點:JS 是「邊跑邊優化」的語言,你寫程式碼的形狀,直接決定 JIT 能不能幫你提速。

二、事件迴圈與微任務:為什麼 Promise 比 setTimeout 快?

JS 是單執行緒語言,靠 事件迴圈(Event Loop) 調度任務。先來一道經典面試題:

console.log('1');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('2');
// 輸出順序:1 → 2 → 3 → 4

規則很簡單:

  • 同步程式碼在呼叫堆疊(Call Stack)上直接執行(輸出 1、2);
  • setTimeout 的回調進入巨任務佇列(Macrotask);
  • Promise.then 的回調進入微任務佇列(Microtask);
  • 每輪事件迴圈:清空呼叫堆疊 → 清空全部微任務 → 取出一個巨任務執行,如此循環。

所以微任務總是「插隊」在下一個巨任務之前。理解這一點,你就懂了為什麼 Vue 的 nextTick 底層用 Promise 實作——它要搶在瀏覽器渲染之前把 DOM 更新完。

三、實戰:寫出對 JIT 友善的程式碼

步驟 1:保持物件「形狀」一致。V8 內部用隱藏類(Hidden Class)記住每個物件的形狀,形狀一致就能共用優化路徑:

// ✅ 好寫法:工廠函式,所有物件形狀一致
function makeUser(name, age) {
  return { name: name, age: age };
}
const users = [makeUser('偉龍', 30), makeUser('海晏', 28)];

// ❌ 壞寫法:逐個動態加屬性,產生多種形狀
const a = {};
a.name = '偉龍';
const b = {};
b.age = 30;   // 形狀分裂,JIT 優化失效

步驟 2:函式參數型別保持穩定。同一個函式一下收數字、一下收字串,會頻繁觸發 Deopt:

// ❌ 多型別混用,容易 Deopt
function add(x, y) { return x + y; }
add(1, 2);
add('a', 'b');

// ✅ 拆成兩個函式,各自型別穩定
function addNum(a, b) { return a + b; }   // 只收數字
function concatStr(a, b) { return a + b; } // 只收字串

步驟 3:用 performance.now() 實測驗證,不要憑感覺。

const start = performance.now();
for (let i = 0; i < 1000000; i++) {
  // 待測程式碼
}
console.log('耗時:', performance.now() - start, 'ms');

特別注意:避免在熱點程式碼裡用 delete 刪除物件屬性,那會改變物件形狀、觸發反優化;也不要在熱迴圈裡塞入會打破引擎假設的邏輯。另外,現代引擎已經很快,動手優化前先量測,避免過早優化反而把程式碼弄髒。

四、小結

理解 JS 引擎,不是要你手寫機器碼,而是讓你寫程式碼時心裡有底:為什麼物件形狀要一致、為什麼微任務會插隊、為什麼型別穩定的函式跑得快。這些底層認知,會在效能關鍵場景(大列表渲染、資料視覺化、動畫)回報給你。

互動話題:你在專案裡遇過哪些「莫名其妙變慢」的 JS 效能問題?後來是怎麼定位的?歡迎留言分享你的排查經驗!