JavaScript 執行模型:從 event loop 到 async/await 的一條推理鏈

文字解析 · 1 支影片 · 產生於 2026-10-01 00:54

🎧 語音解析📝 筆記✍️ 練習

🎧 語音解析 28m46s · TTS 講解與作者原聲交錯;點章節可跳。
邊聽邊看逐句講稿,點任一句從那裡開始
章節(9)

1. Outline

  1. 起點 · 9 JavaScript Concepts That Got Me To Senior Dev
    把「精通基礎」拆成一條可驗收的鏈:event loop 決定執行順序 → execution context 與 scope chain 決定函式看得到什麼 → closure 與 GC 決定記憶體何時釋放 → Promise 解釋誰把任務丟進 micro-task queue → async/await 是它的語法糖 → generator 是語法糖的另一半。

2. YouTuber 的思維推導

9 JavaScript Concepts That Got Me To Senior Dev

theSeniorDev · 20m11s · 字幕 en · vision=on (整支影片是白板式動畫圖解 + 程式碼截圖,作者反覆說「looking at this piece of code」「as you see here」「going back to our promise chain」,說明高度依賴畫面;不看圖無法對照 call stack / microtask queue 的移動與 scope chain 的巢狀關係。) · YouTube

預估 vs 實際耗時
階段預估實際
fetch 6s 2s
segment 1m10s –
shot 49s 6s
analyze 8m45s –
render 5s –

作者的推導是一條「由執行機制往上長」的鏈,而不是九個並列的知識點。他先問「JavaScript 到底是誰在執行、憑什麼決定順序」,答案是 event loop,於是先把 call stack、macro-task queue、micro-task queue 的全景圖畫出來當作全片的座標系。接著他不讓你停在圖上,立刻丟一題 console.log 順序的面試題,逼你用那張圖去預測輸出——這是他反覆使用的手法:先給機制,再用一段程式碼驗收。

驗收過程中他發現推演時把函式當成一個黑盒推進 call stack,於是往下鑽一層問「一個 stack frame 裡到底裝了什麼」,帶出 execution context、scope 與 scope chain;scope chain 決定了函式看得見哪些變數,自然接到「那函式活著的時候這些變數會不會被回收」,於是 closure 與 garbage collection 成為同一個問題的兩面。講完記憶體,他把鏡頭拉回一開始那張圖上還沒解釋的 micro-task queue:是誰把東西丟進去?答案要從歷史講起——callback 時代、callback hell、Promise 作為控制權反轉的解法,而 Promise 的 callback 正是進 micro-task queue 的東西,圖於是閉合。

最後兩層是語法層的收束:async/await 是 Promise 的語法糖,而 async/await 的底層又是 Promise 加上一個可以暫停的函式,也就是 generator。全片的說服力來自這個結構——每個概念都是為了回答前一個概念留下的缺口,而不是因為它出現在面試題庫裡。

推理鏈:每段留給下一段的線索

每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。

  1. 問題設定:沒人說清楚的「基礎」 → 留下一個具體問題:JavaScript 程式碼到底是「誰」在執行、憑什麼決定哪一行先跑?這正是下一段 event loop 要回答的。
  2. Event loop 的整體架構 → 圖看懂了不等於會用。留下的問題是:拿一段同時有 Promise 與 setTimeout 的真實程式碼,這張圖能不能準確預測 console 的輸出順序?
  3. 用 console.log 順序驗證 event loop → 推演時我們把每個函式當成一個方塊推進 call stack。留下的問題是:那個方塊裡面到底裝了什麼?執行一個函式需要的東西不只是函式本身。
  4. Execution context 與 scope chain → scope chain 決定了一個函式看得見哪些變數。留下的問題是:既然函式記得這些變數,那當函式還活著時,這些變數的記憶體還能不能被釋放?
  5. Closure 與垃圾回收 → 記憶體的問題收束了,但第 2 段那張圖上還有一個沒交代的角色:micro-task queue 裡的東西究竟是誰放進去的?答案要從 Promise 出現之前的年代講起。
  6. 從 callback hell 到 Promise → Promise chain 比 callback hell 乾淨,但一長串 .then 讀起來仍然是巢狀的。留下的問題是:能不能讓非同步程式碼長得跟同步程式碼一模一樣?
  7. async/await 是 Promise 的語法糖 → 作者說 async 是「Promise 加上某種能暫停的函式」。留下的問題是:JavaScript 裡什麼樣的函式可以執行到一半停下來、之後再從斷點繼續?
  8. Generator:async/await 的另一半 → 九個概念到這裡連成了一條鏈:從誰執行程式碼,一路推到最上層的語法糖。留下的問題是——這些語言層的知識,該放進整個前端系統的哪個位置?
  9. 結語與下一步

3. 逐段說明

9 JavaScript Concepts That Got Me To Senior Dev

1. 問題設定:沒人說清楚的「基礎」 0:00–0:22

作者指出大家都說要升 senior 就得精通基礎,卻沒人講清楚基礎到底是哪些。他把問題收斂成三個檢驗標準:這些概念是什麼、面試怎麼用、寫 production code 時真的有差嗎。接著宣告本片要講九個每位資深工程師都掌握的 JavaScript 概念,並從 event loop 開始。

承上 從前情提要裡的「你已經會寫 JavaScript、也知道瀏覽器會執行它」出發:你能寫出可以動的程式,但說不出它為什麼照那個順序動。

推理因為讀者已經聽膩了「要精通基礎」這句空話,所以作者第一步不是講概念,而是先幫這句話定義驗收標準:哪些概念算基礎、在面試怎麼被問、寫 production code 時到底有沒有差。有了標準,後面九個概念才不是任意的清單。

AI 補充值得注意他挑的三個檢驗標準其實是同一件事的三個切面:面試題之所以問這些,正是因為它們是「無法用查文件補救」的執行模型知識。API 名稱可以查,但「這行為什麼比那行先印」只能靠腦中有一張正確的模型圖。這也解釋了為什麼整支片的第一個概念是 event loop 而不是語法——他要先給你那張圖。

留給下一段 留下一個具體問題:JavaScript 程式碼到底是「誰」在執行、憑什麼決定哪一行先跑?這正是下一段 event loop 要回答的。

2. Event loop 的整體架構 0:22–2:28

Event loop 是實際執行 JavaScript 的元件,由 call stack、microtask queue 與 macrotask queue 三部分組成,且佇列有優先序。這一切發生在瀏覽器環境裡:DOM 元素上掛的 event listener、Timer API、Promise API 都會把 task 推進佇列;佇列是 FIFO。一次 tick 的流程是:task 進 call stack 拆成 function call 執行,call stack 清空後在結束這次 tick 前先清 microtask queue,然後瀏覽器 render,再跑下一次迭代。

event loop 三大部件(call stack / microtask queue / macrotask queue)的全景圖,後面每個概念都會回到這張圖
0:35 · event loop 三大部件(call stack / microtask queue / macrotask queue)的全景圖,後面每個概念都會回到這張圖
瀏覽器環境圖:DOM、event listener、Timer API、Promise API 如何把 task 推進佇列
1:10 · 瀏覽器環境圖:DOM、event listener、Timer API、Promise API 如何把 task 推進佇列
一次完整 tick 的動線:call stack 清空 → 清 microtask → render → 下一輪,是理解後面所有輸出順序的依據
2:10 · 一次完整 tick 的動線:call stack 清空 → 清 microtask → render → 下一輪,是理解後面所有輸出順序的依據
承上 承上:上一段問「誰在執行 JavaScript、憑什麼決定順序」,作者的答案就是 event loop,並且直接把它的內部零件攤開。

推理因為上一段把問題設定成「執行順序從哪裡來」,所以作者接著不講語法而是畫架構圖:先立 call stack,再加上兩個佇列,最後把瀏覽器環境(DOM、event listener、Timer API、Promise API)接上去,讓「任務從哪裡來、往哪裡去」變成一條可以用手指跟著走的動線。

AI 補充字幕在這裡有明顯的辨識錯誤,把 macro-task queue 全部聽成 micro-task queue(「we have the microtask Q and the microtask Q」)。對照畫面就清楚了:圖上是紅色的 Macro-task Queue 與橘色的 Micro-task Queue 兩個佇列,event listener 與 Timer API 的 callback 進的是 **macro-task queue**,Promise 的 callback 進的才是 micro-task queue。這個區分是下一段那題面試題的全部關鍵,讀字幕時要自己修正回來。 另外補一個作者沒說但決定優先序的規則:一次 tick 裡 macro-task 只取「一個」出來執行,而 micro-task queue 是**清空**——只要裡面還有東西就一直執行,期間新加入的 micro-task 也會在同一輪被吃掉。這就是為什麼無限產生 micro-task 會讓畫面完全卡死,而無限的 setTimeout 不會。

Event Loop

事件迴圈

負責實際執行 JavaScript 的機制,靠一個 call stack 加上數個任務佇列,一輪一輪地決定哪段程式碼何時執行。

Event loop 不是 JavaScript 語言的一部分,而是宿主環境(瀏覽器、Node.js)提供的。這也是為什麼同一段程式碼在瀏覽器與 Node 裡的細節順序可能不同——Node 的 event loop 有 timers、poll、check 等多個階段,還多了 process.nextTick 這個比 micro-task 更優先的佇列。常見誤解是「JavaScript 是單執行緒所以什麼都做不了」:單執行緒的只有 call stack,網路請求、計時器、檔案讀取都在其他執行緒上跑,event loop 只負責把它們的結果排隊送回主執行緒。

相關術語: Call Stack (包含)、Micro-task Queue (包含)

出處:第 2 段「Event loop 的整體架構」

Call Stack

呼叫堆疊

執行同步程式碼的堆疊,函式被呼叫就疊上去、回傳就彈掉,由上往下一路執行到空。

堆疊裡的每一層叫做 stack frame。函式呼叫函式就是往上疊,遞迴太深疊爆了就是 RangeError: Maximum call stack size exceeded。重點是:只要 call stack 還沒清空,佇列裡的任何東西都不會被執行——這就是為什麼一個很慢的同步迴圈會把整個頁面凍住,連按鈕都按不動。

相關術語: Execution Context (每層即一個)

出處:第 2 段「Event loop 的整體架構」

Macro-task Queue

巨集任務佇列

存放 setTimeout、setInterval、event listener 等 callback 的佇列,優先權低於 micro-task queue。

規格書裡它其實叫 task queue,「macro-task」是社群為了跟 micro-task 對照才用的俗稱。每一輪 event loop 只從這裡取出一個任務執行,執行完就去清 micro-task queue,然後才可能 render。所以 setTimeout(fn, 0) 的意思從來不是「立刻執行」,而是「排到下一輪去」。

相關術語: Micro-task Queue (對照組)

出處:第 2 段「Event loop 的整體架構」

Micro-task Queue

微任務佇列

存放 Promise callback(.then/.catch)等微任務的佇列,在每一輪結束前會被整個清空,優先於 macro-task。

除了 Promise,queueMicrotask 與 MutationObserver 也走這條路。「清空」而非「取一個」是它跟 macro-task queue 最大的差別:在清空過程中新產生的 micro-task 會接到隊尾、同一輪被執行完。因此一個會不斷 resolve 自己的 Promise 迴圈可以讓瀏覽器永遠 render 不出來,這是 micro-task 特有的餓死(starvation)風險。

相關術語: Macro-task Queue (對照組)、Promise API (來源)

出處:第 2 段「Event loop 的整體架構」

Promise API

Promise 介面

瀏覽器提供的區域,存放已建立但尚未定案的 promise,決議後把它的 callback 推進 micro-task queue。

在作者的圖上它跟 Timer API 並排放在 event loop 下方,用意是強調:promise 物件本身不住在 call stack 裡,它是宿主環境幫你保管的狀態。promise 被 resolve 的那一刻並不會執行你的 .then,只是把 .then 裡的函式排進 micro-task queue 而已——這個時間差正是下一段面試題的答案來源。

相關術語: Micro-task Queue (推送到)

出處:第 2 段「Event loop 的整體架構」

Tick

一輪迭代

Event loop 的一次完整迭代:執行一個任務、清空 micro-task queue、(可能)render,然後進入下一輪。

一句話記法:「同步跑完 → 清 micro-task → 畫面更新 → 下一輪」。理解 tick 的邊界就理解了絕大多數非同步順序題,因為所有題目本質上都在問「這段程式碼落在第幾個 tick」。

相關術語: Event Loop (的單位)

出處:第 2 段「Event loop 的整體架構」

見仁見智 2:13
「In between the browser will regend and then run the next iteration.」
把「每一輪之間瀏覽器都會 render」講得像必然。實際上 render 是由瀏覽器依畫面更新頻率(一般約 60fps)決定的,多個 tick 可能共用一次 render,頁面在背景分頁時甚至幾乎不 render。正確的說法是「render 只會發生在 tick 之間,但不保證每個 tick 都發生」。
依據: HTML Living Standard, event loop processing model §8.1.7「update the rendering」步驟;瀏覽器只在該次迭代被判定為 rendering opportunity 時才更新畫面
留給下一段 圖看懂了不等於會用。留下的問題是:拿一段同時有 Promise 與 setTimeout 的真實程式碼,這張圖能不能準確預測 console 的輸出順序?

3. 用 console.log 順序驗證 event loop 2:28–5:12

作者拿最經典的面試題:四個 console.log 混著 Promise 與 setTimeout(0),問印出順序。他先示範「照字面由上到下」的直覺答案 1-2-3-4 是錯的,再用前一段的 event loop 圖逐步推:同步的 1 和 4 先印,Promise 的 callback 進 microtask queue 所以印 2,setTimeout 的 callback 要等下一個 tick 才印 3。正確順序是 1、4、2、3。

題目原始程式碼:四個 console.log 的排列,是這一段推理的輸入
2:38 · 題目原始程式碼:四個 console.log 的排列,是這一段推理的輸入
常見的錯誤答案 1-2-3-4,對照組,用來凸顯直覺為何失效
3:00 · 常見的錯誤答案 1-2-3-4,對照組,用來凸顯直覺為何失效
逐行推演圖:Promise callback 被推進 microtask queue 而非立即執行
3:45 · 逐行推演圖:Promise callback 被推進 microtask queue 而非立即執行
最終輸出順序 1、4、2、3 的結論畫面
4:43 · 最終輸出順序 1、4、2、3 的結論畫面
承上 承上:上一段留下「這張圖能不能預測真實程式碼的輸出順序」,這一段就拿最經典的面試題來驗收。

推理因為上一段只給了架構圖,所以作者接著刻意先示範錯誤答案(照字面由上往下的 1-2-3-4),再把同一段程式碼放回圖裡逐行推。先錯後對是他的教法:讓你看見直覺失效在哪一步,你才會記得機制。

AI 補充推演的關鍵在於分辨三種時間點,作者講得快,這裡拆開:**建立**(new Promise 的執行器函式是同步跑的,所以 resolve() 當場就發生)、**排隊**(resolve 只是把 .then 的函式丟進 micro-task queue)、**執行**(要等 call stack 清空)。setTimeout(…, 0) 同理:計時器立刻到期,但 callback 是進 macro-task queue,得等下一輪。 所以順序是 1(同步)→ 4(同步)→ 2(micro-task,本輪結束前)→ 3(macro-task,下一輪)。字幕在這裡同樣把 setTimeout 的去處誤植成 micro-task queue,畫面 s03_225 才是對的:程式碼被高亮的 setTimeout 進的是 call stack 之後排到下一輪,micro-task queue 裡躺著的是 console.log("2")。 順帶一提,如果把 setTimeout 的 0 改成 1000,答案完全不變——這題考的從來不是延遲長短,而是佇列的優先權。

術語:Synchronous Code

Callback

回呼函式

交給別人保管、由對方在某個時間點替你呼叫的函式。

callback 本身不等於非同步——Array.prototype.map 的參數也是 callback,但它是同步立刻執行的。決定它同步或非同步的是「誰在什麼時候呼叫它」。這一段的 .then(() => …) 就是交給 Promise 保管的 callback,所以要等到 promise 決議、又排進 micro-task queue 後才被呼叫。

相關術語: Promise (被掛在)

出處:第 3 段「用 console.log 順序驗證 event loop」

Promise

承諾物件

代表一個「將來才會有結果」的值的物件,帶有內部狀態,你可以事先在上面掛好結果出來要做什麼。

new Promise 的建構子參數(執行器)是**同步**執行的,這點最常被誤解——寫在 new Promise 裡的 console.log 會立刻印出來,非同步的只有 .then 裡的部分。另一個常見誤解是「promise 讓程式變快」:它只是讓等待期間主執行緒可以做別的事,單一請求的時間一點也沒縮短。

相關術語: Micro-task Queue (callback 進入)、Callback (取代了)

出處:第 3 段「用 console.log 順序驗證 event loop」

Synchronous Code

同步程式碼

被推進 call stack 後一路執行到完成、期間不讓出主執行緒的程式碼。

判斷同步與否的實用準則:這行程式碼有沒有可能在「這一輪 call stack 清空之後」才跑?沒有就是同步。面試題裡的 console.log("1") 與 console.log("4") 之所以先印,不是因為它們寫在前面,而是因為它們是同步的。

相關術語: Call Stack (執行於)

出處:第 3 段「用 console.log 順序驗證 event loop」

留給下一段 推演時我們把每個函式當成一個方塊推進 call stack。留下的問題是:那個方塊裡面到底裝了什麼?執行一個函式需要的東西不只是函式本身。

4. Execution context 與 scope chain 5:12–8:38

函式被推進 call stack 時建立的不只是函式本身,而是 execution context(stack frame):函式程式碼、this、arguments、變數對應表(指向 heap 的位址)。接著作者定義 scope:JavaScript 裡一對大括號就是一個 scope,函式、if、for、else 層層巢狀;scope chain 是某個位置能存取到的所有 scope,只能由內往外,父 scope 看不到子 scope 的變數。

execution context 的組成清單(函式碼、this、arguments、變數對應到 heap),說明「執行一個函式需要什麼」
5:30 · execution context 的組成清單(函式碼、this、arguments、變數對應到 heap),說明「執行一個函式需要什麼」
calculateNetProfit 範例程式碼,對照 global context 與 local context 各包含哪些東西
6:10 · calculateNetProfit 範例程式碼,對照 global context 與 local context 各包含哪些東西
大括號 = scope 的巢狀圖(function → if → for / else),scope chain 的視覺定義
6:50 · 大括號 = scope 的巢狀圖(function → if → for / else),scope chain 的視覺定義
formatNetAmount 能存取哪些 scope 的示意,示範「由內往外」的方向性
7:40 · formatNetAmount 能存取哪些 scope 的示意,示範「由內往外」的方向性
承上 承上:上一段推演時把函式當成一個方塊推進 call stack,這一段就打開那個方塊,看執行一個函式到底需要準備什麼。

推理因為上一段留下「stack frame 裡裝了什麼」,所以作者接著定義 execution context:函式碼、this、arguments、以及變數對應表(指向 heap 的位址)。而「變數對應表包含哪些變數」這個問題沒辦法在原地回答,於是他順勢往下定義 scope 與 scope chain——scope chain 正是那張表的來源。

AI 補充作者說「大括號就是 scope」是好用的直覺,但要補一個前提才精確:這條規則對 let / const 成立,對 var 不成立。var 只認 function scope,所以在 if 或 for 的大括號裡宣告的 var 會漏到整個函式,這正是 ES6 引入 let/const 的原因,也是老程式碼裡迴圈變數出錯的經典來源。 另外把「只能由內往外」翻譯成更實用的說法:內層是外層的**延伸**,不是複本。內層函式讀到的是外層變數當下的值,不是宣告時的快照——這個性質在下一段的 closure 會直接變成主角。 畫面 s04_370 那段投影片有個小筆誤:程式碼寫 const netSales = calculateNetSales(...) 但下一行用的是 salesAfterVAT,這個變數並不存在,照抄會 ReferenceError。看圖時把它當 netSales 就好,不影響 scope 的說明。

Execution Context

執行環境

函式被執行時建立的一整包資訊:函式碼、this 的值、arguments、以及能存取到的變數對應表。

每呼叫一次函式就新建一個 execution context,函式回傳就銷毀——除非有 closure 抓著它不放。除了函式的 execution context,還有一個開分頁時就建立的 global execution context,裡面裝著 fetch、alert、document 這些全域東西。理解它最大的實益是理解 this:this 不屬於函式本身,而是屬於這次呼叫的 execution context,所以同一個函式用不同方式呼叫,this 會不同。

相關術語: Stack Frame (同一件事)、Scope Chain (包含)

出處:第 4 段「Execution context 與 scope chain」

Stack Frame

堆疊框

Execution context 在 call stack 上的那一格,是同一件事的另一個名字。

「stack frame」是從編譯器/作業系統的角度講的說法,「execution context」是 ECMAScript 規格的說法。你在 DevTools 的 Call Stack 面板看到的每一行、錯誤訊息裡 stack trace 的每一層,都是一個 stack frame——所以看懂 stack trace 其實就是在逆向讀 call stack。

相關術語: Call Stack (位於)

出處:第 4 段「Execution context 與 scope chain」

Scope

作用域

變數有效的範圍;在 JavaScript 裡一對大括號(函式、if、for、else)就開出一個新的 scope。

大括號規則對 let / const 成立,var 例外——var 只認函式邊界。另外要區分 block scope(if、for 的大括號)與 function scope(函式的大括號),面試常考的迴圈裡 setTimeout 印出同一個數字的題目,根因就是 var 沒有 block scope。

相關術語: Scope Chain (串成)

出處:第 4 段「Execution context 與 scope chain」

Scope Chain

作用域鏈

從目前位置由內往外串起來的所有 scope,決定這裡能存取到哪些變數。

查找方向是單向的:內層看得到外層,外層看不到內層,兄弟 scope(if 與 else)彼此也看不到。實際查找是「就近優先」——內層有同名變數就遮住外層的,這就是 shadowing。scope chain 在函式**宣告**時就決定了,不是呼叫時才決定,這個性質叫 lexical scoping,也是下一段 closure 的前提。

相關術語: Closure (前提)、Scope (由…組成)

出處:第 4 段「Execution context 與 scope chain」

Heap

堆積記憶體

存放物件與變數實際內容的記憶體區域;stack frame 裡放的只是指向 heap 的位址。

stack 與 heap 的分工:stack 小、快、隨函式回傳自動清掉;heap 大、慢、要靠 garbage collection 回收。這也解釋了為什麼把物件指派給另一個變數是「共用同一份」而不是複製——兩個變數持有的是同一個 heap 位址。

相關術語: Garbage Collection (回收對象)

出處:第 4 段「Execution context 與 scope chain」

留給下一段 scope chain 決定了一個函式看得見哪些變數。留下的問題是:既然函式記得這些變數,那當函式還活著時,這些變數的記憶體還能不能被釋放?

5. Closure 與垃圾回收 8:38–11:24

Closure 是函式記住它宣告時 lexical scope 裡的變數,只要函式還活著,那些變數就不能被回收。但現代編譯器只會封閉「真的有被用到」的變數。作者用一段程式碼問哪個變數會被 GC:calculateNetProfit 掛在 DOM 元素上等於永生,它用到的 calculateNetSales 與 taxRateCorporate、以及被間接引用的 taxRateVAT 都留著,只有 taxRateDividends 可被標記回收。GC 演算法週期性掃 heap,只做「軟刪除」——標記為可覆寫,而非真的抹掉。

題目程式碼:哪一個變數會被 garbage collect,這一段的推理起點
9:20 · 題目程式碼:哪一個變數會被 garbage collect,這一段的推理起點
引用鏈的追蹤圖:DOM → calculateNetProfit → calculateNetSales → taxRateVAT,看得出誰被鎖住
9:55 · 引用鏈的追蹤圖:DOM → calculateNetProfit → calculateNetSales → taxRateVAT,看得出誰被鎖住
只有 taxRateDividends 可回收的結論畫面
10:25 · 只有 taxRateDividends 可回收的結論畫面
heap 上「軟刪除」的示意:標記為可重新配置而非清空
10:55 · heap 上「軟刪除」的示意:標記為可重新配置而非清空
承上 承上:上一段留下「函式還活著時,它 scope chain 上的變數能不能被釋放」,closure 與 garbage collection 就是這個問題的正反兩面。

推理因為上一段已經建立了 scope chain,所以作者接著只要加一句話就定義出 closure:函式記住它宣告時 lexical scope 裡的變數。然後他立刻把理論拉回工程現實——理論上整條 scope chain 都不能回收,實務上編譯器只封閉真正被用到的變數——再用一題「哪個變數會被回收」做驗收,手法跟第 3 段完全一樣。

AI 補充作者的追蹤路徑值得原樣記下來,因為它就是判斷記憶體洩漏的標準做法:從**永生的根**開始往外追。這裡的根是 DOM 元素上的 onclick,只要分頁不關就活著 → 它抓著 calculateNetProfit → 這個函式用到 taxRateCorporate 與 calculateNetSales → calculateNetSales 又用到 taxRateVAT,於是這四個都動不了;沒有任何人引用的 taxRateDividends 才是唯一可回收的。 把它反過來用就是實務上最常見的洩漏原因:忘了 removeEventListener 的 listener、沒有 clearInterval 的計時器、閉包裡不小心抓著一整個大物件——它們都是「一個永生的根抓著一條你沒注意到的鏈」。 「soft delete」是作者自創的比喻,不是正式術語;正式的說法是 mark-and-sweep:標記所有從根可達的物件,其餘視為垃圾、其空間可被重新配置。他強調「不是真的抹掉」是對的,但要小心別推論成「記憶體不會真的還給系統」——引擎確實會在整理(compaction)後把大塊空閒記憶體還給作業系統。

術語:Garbage Collection

Closure

閉包

函式會記住它被宣告時所在的 lexical scope 裡的變數,只要函式還被引用,那些變數就不能被回收。

closure 不是你要特別「做」出來的東西——只要函式裡用到了外層變數,closure 就自動存在了。常見誤解是「closure 會把整個外層 scope 都留住」:理論上是,但現代引擎只封閉真正被引用到的變數(作者這裡講得很準確)。closure 最典型的用途是製造私有狀態,例如工廠函式回傳的函式共用一個外層計數器——這也是下一段 generator「有狀態的函式」的前身。

相關術語: Scope Chain (記住的是)、Garbage Collection (阻止)

出處:第 5 段「Closure 與垃圾回收」

Lexical Scope

語彙作用域

作用域由程式碼寫在哪裡決定,而不是由函式在哪裡被呼叫決定。

「lexical」意思是「照字面、照原始碼的排版」。對照組是 dynamic scope(依呼叫者決定),JavaScript 的變數查找是 lexical 的,但 this 的綁定卻是 dynamic 的——這個不一致正是箭頭函式存在的理由:箭頭函式讓 this 也變成 lexical 的。

相關術語: Closure (的基礎)

出處:第 5 段「Closure 與垃圾回收」

Garbage Collection

垃圾回收

引擎週期性掃描 heap,把沒有任何引用指向的變數標記為可用記憶體的機制。

現代引擎用的是 mark-and-sweep:從根(全域物件、call stack 上的變數、DOM 樹)出發標記所有可達的物件,剩下的一律回收;早期的 reference counting 因為處理不了循環引用而被淘汰。GC 的時機不可預測也無法可靠地手動觸發,所以「避免產生垃圾」比「想辦法回收」重要得多。

相關術語: Heap (作用於)、Closure (被阻擋於)

出處:第 5 段「Closure 與垃圾回收」

Soft Delete

軟刪除

作者用來形容 GC 的比喻:記憶體空間只是被標記成可重新配置,內容並沒有被立刻清空。

這不是 JavaScript 規格裡的術語(soft delete 在資料庫語境是另一回事:用 deleted_at 欄位標記而不真的刪除列)。用在這裡的意思接近 free list 管理:空間掛回可用清單,等下一次配置覆寫。實務上的意義是別指望「變數設成 null 就會立刻省下記憶體」。

相關術語: Garbage Collection (的比喻)

出處:第 5 段「Closure 與垃圾回收」

見仁見智 10:13
「And so the only variable that can be garbage collected is tax rate dividends.」
在他的例子裡 taxRateDividends 是**最上層的 const**,屬於 global/module scope。只要這支 script 的環境還活著(分頁沒關、模組沒卸載),全域繫結一般不會被回收,實際能不能收掉取決於引擎最佳化與這段程式碼是否被包在函式裡。這個結論在「把整段包進一個函式」的情境下完全正確,直接套到全域變數則過於武斷。
依據: V8 的 mark-and-sweep 以全域物件與模組環境為 GC root;ECMAScript 規格未要求回收仍可達的全域繫結
留給下一段 記憶體的問題收束了,但第 2 段那張圖上還有一個沒交代的角色:micro-task queue 裡的東西究竟是誰放進去的?答案要從 Promise 出現之前的年代講起。

6. 從 callback hell 到 Promise 11:24–15:33

作者回溯歷史:沒有 Promise 之前只能用 callback,把一段程式碼交給對方「之後幫我執行」,舊版 fetch/XMLHttpRequest 就是這樣,並形成 error-first 慣例(第一個參數是錯誤,第二個才是資料)。callback 的問題是層層巢狀難以除錯,也就是 callback hell。Promise 反轉了控制權:不是把 callback 交出去,而是拿回一個帶內部狀態(pending → fulfilled / rejected)的物件,自己往上掛 .then / .catch,而這些 callback 一樣是被推進 microtask queue,在當前 tick 結束前執行。

error-first callback 的函式簽章圖,說明舊 API 的約定
12:10 · error-first callback 的函式簽章圖,說明舊 API 的約定
network thread → callback 被推回 call stack 的流程,把 callback 接回 event loop 架構
13:10 · network thread → callback 被推回 call stack 的流程,把 callback 接回 event loop 架構
callback hell 的巢狀程式碼實景,是 Promise 要解決的問題本體
13:20 · callback hell 的巢狀程式碼實景,是 Promise 要解決的問題本體
Promise 狀態機:pending / fulfilled / rejected 與對應的 then / catch
14:05 · Promise 狀態機:pending / fulfilled / rejected 與對應的 then / catch
Promise 與 event loop 合體的動線圖:resolve 後 callback 進 microtask queue
15:00 · Promise 與 event loop 合體的動線圖:resolve 後 callback 進 microtask queue
承上 承上:上一段留下「micro-task queue 裡的東西是誰放進去的」,作者的回答是 Promise,但他先把 Promise 出現前的世界講一遍。

推理因為要說明 Promise 解決了什麼,所以作者先重建問題本身:callback 時代你只能把一段程式碼交出去、祈禱對方在對的時機呼叫它,巢狀一深就是無法閱讀也無法除錯的 callback hell。有了這個對照組,Promise 的價值才不是「比較新的寫法」,而是控制權的反轉——不是把 callback 交出去,而是拿回一個物件,自己往上掛。

AI 補充「inversion of control」這個詞作者只輕輕帶過,但它是這一段真正的重點。callback 模式下,你的程式碼**何時執行、執行幾次、拿到什麼參數**全由對方決定;換成 Promise,狀態機在你手上,而規格保證它只會決議一次、callback 只會被呼叫一次、而且必定是非同步的。callback hell 難以除錯的根因也在這裡:錯誤沒有統一的傳遞路徑,每一層都得自己寫 if (err),漏一層就靜默失敗;Promise 則讓 rejection 沿著鏈往下傳,一個 .catch 收尾。 把它接回第 2 段那張圖就閉合了:promise 被 resolve 時,它掛著的 callback 不是立刻執行,而是被推進 micro-task queue,等這一輪 call stack 清空後才跑。這正是第 3 段那題「2 為什麼排在 4 後面卻在 3 前面」的機制來源。 error-first 慣例(第一個參數是錯誤、第二個才是資料)今天仍然活著——Node.js 的傳統 API 幾乎都是這個形狀,util.promisify 之所以能通用轉換,就是因為它假設你遵守這個約定。

術語:Error-first CallbackPromise States

Callback Hell

回呼地獄

callback 一層套一層形成的金字塔狀程式碼,難讀、難除錯、錯誤處理必須層層重複。

又叫 pyramid of doom,看縮排就認得出來。它的痛點不只是難看:每一層都要獨立處理錯誤,漏掉一層就變成靜默失敗;而且巢狀之間共用變數會讓 closure 抓住大量不該留的資料。畫面 s06_800 那段三層巢狀、每層各有 try/catch 與 if (err) 的程式碼就是標準樣本。

相關術語: Promise (被…取代)

出處:第 6 段「從 callback hell 到 Promise」

Error-first Callback

錯誤優先回呼

callback 慣例:第一個參數放錯誤(沒錯就給 null),第二個之後才是資料。

這是 Node.js 生態的事實標準,也是 util.promisify 能自動把舊 API 轉成 Promise 的前提。它的弱點是純屬約定、編譯器不強制,所以忘記檢查第一個參數的錯誤在舊程式碼裡極常見——這也是 Promise 把錯誤處理內建進語言的動機之一。

相關術語: Callback Hell (加劇)

出處:第 6 段「從 callback hell 到 Promise」

Inversion of Control

控制權反轉

從「把 callback 交給對方、由對方決定何時呼叫」,變成「拿回一個 promise 物件、由自己決定掛什麼上去」。

作者說 Promise 是「某種意義上的控制權反轉」,嚴格說是**把已經被反轉出去的控制權拿回來**。實際保障來自規格:一個 promise 只能決議一次、callback 至多被呼叫一次、且一定是非同步呼叫。callback 模式沒有任何一條是保證的,第三方函式庫呼叫你的 callback 兩次是真實會發生的 bug。

相關術語: Promise (由…實現)

出處:第 6 段「從 callback hell 到 Promise」

Promise States

Promise 狀態

promise 的三種內部狀態:pending(等待中)、fulfilled(已成功)、rejected(已失敗),只能從 pending 單向轉一次。

「settled」是 fulfilled 與 rejected 的統稱(Promise.allSettled 的命名由來)。狀態不可逆是 Promise 最重要的保證:一旦決議,之後再呼叫 resolve 或 reject 都會被忽略,而且新掛上去的 .then 仍會拿到已經定案的結果——這就是為什麼可以在請求完成後才掛 .then 也不會漏接。

相關術語: Promise (屬於)、Micro-task Queue (決議時推進)

出處:第 6 段「從 callback hell 到 Promise」

見仁見智 12:03
「So the old implementation of our fetch API was not returning a promise like it does today, but rather asking you to pass a call back to it.」
瀏覽器的 fetch() 從 2015 年首次登場就是回傳 Promise,從來沒有 callback 版本。callback 時代的網路 API 是 XMLHttpRequest(他投影片上寫的其實是自己包的 function fetch(url, callback))。如果把這句聽成「標準 fetch 曾經是 callback 式的」就會記錯歷史;他要表達的應該是「以前自己包的抓資料函式長這樣」。
依據: Fetch Standard 於 2015 年發布,Chrome 42 / Firefox 39 起支援,介面自始即為 Promise-based
留給下一段 Promise chain 比 callback hell 乾淨,但一長串 .then 讀起來仍然是巢狀的。留下的問題是:能不能讓非同步程式碼長得跟同步程式碼一模一樣?

7. async/await 是 Promise 的語法糖 15:33–16:33

async/await 讓你不用顯式建立 Promise 就能用它,底層會被翻譯回 Promise(作者稱之為 syntax sugar / polyfill)。同一段 promise chain 只要把函式標成 async、在每個 promise 前加 await,就能寫成近乎同步的形狀。程式碼規模越大,從 callback hell → promise chain → async/await 的可讀性差距越明顯。

promise chain 與 async/await 的並排對照,同一段邏輯兩種寫法
16:02 · promise chain 與 async/await 的並排對照,同一段邏輯兩種寫法
承上 承上:上一段留下「能不能讓非同步程式碼長得像同步」,async/await 就是這個問題的語法層答案。

推理因為上一段已經確立 Promise 是底層機制,所以作者接著把 async/await 定位成「不用顯式建立 Promise 就能用 Promise 的寫法」,並強調它會被翻譯回 Promise——這一步很關鍵,因為它讓讀者知道前面學的 event loop 與 micro-task 規則完全不需要重學。

AI 補充「翻譯回 Promise」具體是什麼意思,值得講明白:await 右邊的東西會先被包成 Promise,然後**函式在這裡暫停、把控制權交回 call stack**,等這個 promise 決議後,剩下的函式本體被當成一個 micro-task 排隊執行。所以 await 之後的程式碼永遠在下一個 micro-task 才跑——這就是為什麼 async 函式裡 await 前的部分是同步執行的。 實務上最常被漏掉的一點:把多個彼此無關的 await 一行行排下來,等於強迫它們序列化。三個各 1 秒的請求寫成三行 await 要 3 秒,改成 await Promise.all([...]) 只要 1 秒。async/await 讓程式碼看起來像同步,也讓人不小心真的寫成同步。

async/await

非同步語法

用 async 標記函式、用 await 等待 promise 的語法,讓非同步流程寫起來跟同步程式碼一樣是一行接一行。

兩個容易踩的點:一、async 函式一定回傳 Promise,就算你 return 一個數字也會被包起來;二、await 只在 async 函式(或 ES2022 之後的模組頂層)裡合法。錯誤處理則從 .catch 換成熟悉的 try/catch,這也是它最大的可讀性紅利之一。

相關術語: Promise (底層即)、Generator Function (實作用到)

出處:第 7 段「async/await 是 Promise 的語法糖」

Syntax Sugar

語法糖

不新增任何能力、只讓既有機制更好寫好讀的語法。

JavaScript 裡的其他例子:class(底層仍是 prototype)、解構賦值、樣板字串。判準是「拿掉它之後能不能用既有語法寫出等價的東西」——能,就是語法糖。async/await 完全符合:它能做的事 Promise 都做得到,只是寫起來痛苦得多。

相關術語: async/await (是一種)

出處:第 7 段「async/await 是 Promise 的語法糖」

Polyfill

填補程式

用既有語法實作出舊環境缺少的新功能,讓舊瀏覽器也能使用該 API。

polyfill 與 syntax sugar 是兩件不同的事:polyfill 是**執行期**補上缺少的 API(例如用一段 JS 實作 Array.prototype.includes),語法糖是語言本身就提供的等價寫法;把新語法降級成舊語法的是 Babel 這類 transpiler。作者在這裡把兩者混用了(見本段勘誤)。

相關術語: Syntax Sugar (對照組)

出處:第 7 段「async/await 是 Promise 的語法糖」

確定錯誤/已過時 15:45
「And we also call that polyfill or syntax sugar.」
async/await 是語言原生語法,是 syntax sugar,但不是 polyfill。polyfill 指的是在缺少某功能的舊環境裡、用既有語法自己實作出該 API;把新語法轉成舊語法的工具叫 transpiler(Babel)。他在第 8 段展示的 asyncPolyfill 示意圖是「用 generator 模擬 async」的教學範例,那才勉強算 polyfill,但語言內建的 async/await 本身不是。
依據: MDN Glossary: Polyfill(以 JavaScript 實作瀏覽器原生未提供的功能)vs. Syntactic sugar;async/await 為 ES2017 原生語法
留給下一段 作者說 async 是「Promise 加上某種能暫停的函式」。留下的問題是:JavaScript 裡什麼樣的函式可以執行到一半停下來、之後再從斷點繼續?

8. Generator:async/await 的另一半 16:33–19:22

async 由兩樣東西組成:一個 Promise,加上一個 generator function。Generator 是「有狀態、可暫停」的函式:加上 star 後呼叫它只會建立 iterator 物件而不執行,每次 next() 才往前跑到下一個 yield 就回傳並暫停,下次從斷點續跑。作者用無限 counter 示範 1、2、3 一路數下去,也提到可以讓它 done。最後把兩半接起來:async 函式裡的每個 await 大致對應底層 generator 的一個 yield,外層 promise 遞迴呼叫 generator,所有 yield 跑完後 promise 才 resolve。

generator 的 counter 範例程式碼(function* 與 yield),這一段所有推理都以它為例
17:10 · generator 的 counter 範例程式碼(function* 與 yield),這一段所有推理都以它為例
第一次 next() 執行到 yield 後暫停的狀態圖,示範「函式有狀態」
17:42 · 第一次 next() 執行到 yield 後暫停的狀態圖,示範「函式有狀態」
第二、三次呼叫從斷點續跑、count 遞增的畫面,看出暫停/恢復的節奏
18:05 · 第二、三次呼叫從斷點續跑、count 遞增的畫面,看出暫停/恢復的節奏
async = 外層 promise + 內部 generator 遞迴的整體關係圖,把本片前後概念收在一起
19:00 · async = 外層 promise + 內部 generator 遞迴的整體關係圖,把本片前後概念收在一起
承上 承上:上一段留下「什麼樣的函式可以暫停再續跑」,generator function 就是 JavaScript 給的答案,也是 async/await 底層的另一半。

推理因為上一段說 async 是 Promise 加上可暫停的函式,所以作者接著用最小的例子(一個無限 counter)把 generator 的三個關鍵行為演一遍:呼叫它只拿到 iterator 不執行、每次 next() 跑到 yield 就回傳並暫停、下次從斷點續跑。演完再把它接回 async/await,整支影片的鏈就閉合了。

AI 補充把 generator 跟第 5 段的 closure 對照著看,這個概念會突然變得平凡:closure 讓函式記住外層的變數,generator 更進一步讓函式記住**自己執行到哪一行**。它保存的不只是資料,是整個 execution context——所以第二次 next() 時,count 還在、while 迴圈的位置也還在。 yield 跟 return 的差別要講清楚:yield 回傳值並凍結,return 回傳值並結束(此後 done 為 true)。作者提到「可以讓它停」指的就是加一個 if 之後改用 return。 最後那張 s08_1140 的對照圖是全片的收束:把 async 函式裡的每個 await 換成 yield,外面套一層驅動器不斷呼叫 next()、拿到 promise 就等它決議再把結果餵回去,就得到等價的行為。真正的實作比這複雜(要處理 throw、return、以及 thenable),但心智模型正是如此——這也解釋了 async/await 為何能在 ES2017 之前用 Babel + regenerator 在舊瀏覽器上跑。 順帶修正一個時間感:generator 是 ES6(2015)的功能,跟 Promise 同期,並不算新。

Generator Function

產生器函式

用 function* 宣告的函式,呼叫時不執行、只回傳一個 iterator,之後每次 next() 才往前跑到下一個 yield 並暫停。

它是 JavaScript 裡唯一能讓函式「執行到一半交還控制權、之後原地續跑」的語法(協程 coroutine 的一種)。除了當 async/await 的底層,實務用途包括:產生無限序列(費氏數列、ID 產生器)、把大量資料切成一次一筆處理以免佔滿記憶體、以及 redux-saga 那種可測試的副作用流程。next() 還能傳值進去,成為 generator 內部 yield 運算式的結果——雙向溝通正是驅動 async 所必需的。

相關術語: async/await (底層之一)、Closure (更進一步的)

出處:第 8 段「Generator:async/await 的另一半」

yield

產出

generator 裡的暫停點:回傳一個值給呼叫者,並把函式凍結在這一行等待下次 next()。

跟 return 的差別是「凍結 vs. 結束」:yield 之後函式還活著,區域變數與執行位置都保留;return 之後 iterator 的 done 變成 true,再呼叫 next() 也只會拿到 undefined。在 async/await 的對照裡,每個 await 大致對應一個 yield。

相關術語: Generator Function (屬於)

出處:第 8 段「Generator:async/await 的另一半」

Iterator

迭代器

有 next() 方法、每次呼叫回傳 { value, done } 的物件;generator 呼叫後拿到的就是它。

iterator 是 for...of、展開運算子、解構賦值共用的協定,陣列、字串、Map、Set 都內建。所以 generator 產生的 iterator 可以直接被 for...of 消費——這也是為什麼寫一個自訂的可迭代物件,最省事的做法就是給它一個 generator 當 [Symbol.iterator]。

相關術語: Generator Function (由…產生)

出處:第 8 段「Generator:async/await 的另一半」

見仁見智 16:35
「Now generator functions are a relatively new feature of the language」
generator 是 ES6(ECMAScript 2015)的功能,跟 Promise、let/const、箭頭函式同一批,到 2025 年已滿十年,所有現代瀏覽器與 Node 都原生支援。說它「相對新」在 2015 年成立,今天比較準確的說法是「相對少用」而不是相對新。
依據: ECMAScript 2015 (ES6) 規格納入 generator function;Chrome 39 / Firefox 26 / Node 4 起支援
留給下一段 九個概念到這裡連成了一條鏈:從誰執行程式碼,一路推到最上層的語法糖。留下的問題是——這些語言層的知識,該放進整個前端系統的哪個位置?

9. 結語與下一步 19:22–20:11

作者說明這支只是粗略帶過底層 polyfill,並預告第二部。他強調光懂基礎還不夠:資深工程師還被期待能推理前端架構,因此建議接著看前端架構與「15 個前端概念」兩支影片,把個別語言概念放回整體系統的脈絡。

承上 承上:上一段把九個概念收束成一條從執行機制到語法糖的鏈,這一段作者說明這條鏈的邊界在哪、以及接下來該往哪走。

推理因為前八段已經把語言層的執行模型建立完整,所以作者最後刻意把層級往上拉:他點出「懂基礎還不夠,資深工程師還被期待能推理前端架構」,並指向自己另外兩支影片,等於承認這支片只涵蓋了資深能力的一個維度。

AI 補充作者在片中兩次說「這支只是粗略帶過、想看手刻 polyfill 就留言」,這其實反映了一個真實的分界:面試會考的是**能不能用模型解釋現象**(第 3、5 段那種題型),而不是能不能背出規格細節。所以複習這支片的正確方式不是重看,而是找幾段自己專案裡的非同步程式碼,用第 2 段那張圖逐行推一次輸出順序——推得出來就是真的懂了。 如果要自己補完這條鏈,最欠缺的一塊是 this 與原型鏈:它跟 execution context 是同一個機制的兩面(第 4 段提過 execution context 裡有 this 的值),卻沒有在這支片裡展開。

留給下一段 總結收束。

4. 總結

這支影片的九個概念不是並列的清單,而是一條首尾相接的鏈。作者先問「JavaScript 到底是誰在執行、憑什麼決定順序」,答案是 event loop——call stack 跑同步碼,macro-task queue 與 micro-task queue 分別排隊,每一輪清空 call stack 後先清 micro-task 才可能 render。這張圖立刻被拿去驗收:一題混著 Promise 與 setTimeout(0) 的 console.log 順序題,正確答案 1、4、2、3 完全由佇列優先權決定。推演過程中函式被當成黑盒推進 call stack,於是往下鑽一層變成 execution context(函式碼、this、arguments、變數對應表),而變數對應表的來源是 scope chain——大括號開 scope、查找只能由內往外。scope chain 決定函式記得哪些變數,這一句話就是 closure 的定義;反過來看同一件事就是 garbage collection:從永生的根(掛在 DOM 上的 handler)往外追引用鏈,追不到的才可回收。講完記憶體後鏡頭拉回那張圖上還沒解釋的 micro-task queue——是誰把東西放進去?答案要從 callback 時代講起:把程式碼交出去、祈禱對方在對的時機呼叫,巢狀一深就是 callback hell;Promise 反轉控制權,讓你拿回一個有 pending/fulfilled/rejected 狀態的物件,而它決議時推進的正是 micro-task queue,圖於是閉合。最後兩層是語法收束:async/await 是 Promise 的語法糖,而它的底層是 Promise 加上一個能暫停的函式,也就是 generator——每個 await 大致對應一個 yield。整條鏈的價值在於:這些概念之所以是「基礎」,不是因為常考,而是因為它們彼此互為前提,缺一個就解釋不了下一個。

勘誤總整理

確定錯誤/已過時見仁見智(取決於版本或情境)

段落原話(transcript 逐字)說明
7. async/await 是 Promise 的語法糖
15:45
「And we also call that polyfill or syntax sugar.」async/await 是語言原生語法,是 syntax sugar,但不是 polyfill。polyfill 指的是在缺少某功能的舊環境裡、用既有語法自己實作出該 API;把新語法轉成舊語法的工具叫 transpiler(Babel)。他在第 8 段展示的 asyncPolyfill 示意圖是「用 generator 模擬 async」的教學範例,那才勉強算 polyfill,但語言內建的 async/await 本身不是。
依據: MDN Glossary: Polyfill(以 JavaScript 實作瀏覽器原生未提供的功能)vs. Syntactic sugar;async/await 為 ES2017 原生語法
2. Event loop 的整體架構
2:13
「In between the browser will regend and then run the next iteration.」把「每一輪之間瀏覽器都會 render」講得像必然。實際上 render 是由瀏覽器依畫面更新頻率(一般約 60fps)決定的,多個 tick 可能共用一次 render,頁面在背景分頁時甚至幾乎不 render。正確的說法是「render 只會發生在 tick 之間,但不保證每個 tick 都發生」。
依據: HTML Living Standard, event loop processing model §8.1.7「update the rendering」步驟;瀏覽器只在該次迭代被判定為 rendering opportunity 時才更新畫面
5. Closure 與垃圾回收
10:13
「And so the only variable that can be garbage collected is tax rate dividends.」在他的例子裡 taxRateDividends 是**最上層的 const**,屬於 global/module scope。只要這支 script 的環境還活著(分頁沒關、模組沒卸載),全域繫結一般不會被回收,實際能不能收掉取決於引擎最佳化與這段程式碼是否被包在函式裡。這個結論在「把整段包進一個函式」的情境下完全正確,直接套到全域變數則過於武斷。
依據: V8 的 mark-and-sweep 以全域物件與模組環境為 GC root;ECMAScript 規格未要求回收仍可達的全域繫結
6. 從 callback hell 到 Promise
12:03
「So the old implementation of our fetch API was not returning a promise like it does today, but rather asking you to pass a call back to it.」瀏覽器的 fetch() 從 2015 年首次登場就是回傳 Promise,從來沒有 callback 版本。callback 時代的網路 API 是 XMLHttpRequest(他投影片上寫的其實是自己包的 function fetch(url, callback))。如果把這句聽成「標準 fetch 曾經是 callback 式的」就會記錯歷史;他要表達的應該是「以前自己包的抓資料函式長這樣」。
依據: Fetch Standard 於 2015 年發布,Chrome 42 / Firefox 39 起支援,介面自始即為 Promise-based
8. Generator:async/await 的另一半
16:35
「Now generator functions are a relatively new feature of the language」generator 是 ES6(ECMAScript 2015)的功能,跟 Promise、let/const、箭頭函式同一批,到 2025 年已滿十年,所有現代瀏覽器與 Node 都原生支援。說它「相對新」在 2015 年成立,今天比較準確的說法是「相對少用」而不是相對新。
依據: ECMAScript 2015 (ES6) 規格納入 generator function;Chrome 39 / Firefox 26 / Node 4 起支援

5. 推薦三個下一步

1. 往下挖深:把 async/await 手刻出來

影片第 8 段只給了「await 大致對應 yield」的示意圖,真正的驅動器(怎麼處理 throw、return、thenable)被作者明說跳過了。自己寫一次 polyfill 是把這條鏈焊死的最快方式。

YouTube 搜尋:async await polyfill generator implementation how async await works under the hood javascript co library generator promise runner

2. 往旁邊對照:this、原型鏈與記憶體洩漏實測

第 4 段提到 execution context 裡有 this 的值卻沒展開,而 this 與原型鏈是同一個機制的另一面;第 5 段的 GC 推理也值得用 DevTools 的 Memory 面板實際驗一次,看看全域 const 到底會不會被回收。

YouTube 搜尋:javascript this binding prototype chain explained chrome devtools memory leak heap snapshot tutorial javascript garbage collection mark and sweep v8

3. 往上應用:把執行模型放進前端架構與效能

作者自己在結尾指出「懂基礎還不夠,資深工程師要能推理架構」。long task 卡住 main thread、INP 指標、Web Worker 拆分運算,全都是這支片的 event loop 圖在真實系統裡的直接後果。

YouTube 搜尋:main thread long tasks INP web performance web workers offload main thread javascript frontend architecture senior engineer system design

📄 全部影片 · 主題區: JavaScript 執行模型