JavaScript execution context:從 call stack 上的一格,拆到 realm、environment record、scope chain 與 closure
文字解析 · 1 支影片 · 產生於 2026-10-01 00:54
章節(12)
1. Outline
- 起點 · JavaScript Visualized - Execution Contexts
用一段十行的 script 走完載入到執行結束的全程,把 execution context 這一格拆開,順帶把 hoisting、scope chain、closure 還原成同一套零件的三個側面。
2. YouTuber 的思維推導
JavaScript Visualized - Execution Contexts
Lydia Hallie · 11m40s · 字幕 en · vision=on (使用者指定 vision=true;且整支影片是動畫圖解,作者不斷指著畫面上的 execution context 結構圖說明各元件之間的指向關係,不看圖幾乎無法對照。) · YouTube
預估 vs 實際耗時
| 階段 | 預估 | 實際 |
|---|---|---|
| fetch | 6s | – |
| segment | 53s | 5m00s |
| shot | 30s | 5s |
| analyze | 5m00s | 10m26s |
| render | 5s | – |
作者的出發點是一句幾乎所有人都聽過、卻沒被追問過的話:載入 script 或呼叫函式時,會有東西被推上 call stack。她先把這句話拆開——call stack 其實只是 execution context 的堆疊——於是問題就變成「execution context 到底是什麼」。接著她刻意不走「列規格條目」的路,而是挑一段九行的 script,宣布要把它從載入到結束完整走一遍。
走之前先立兩根柱子:每個 execution context 都會經過 creation phase 與 execution phase 兩個階段;而 execution context 本身不存值,真正記帳的是 environment record。有了這兩根柱子,她由外而內鋪陳靜態結構:realm(彼此隔離的執行環境)→ intrinsics(內建物件的實體)→ global object(把實體曝露成名字,並收下 var 與全域函式宣告)→ global environment record(用 object record 與 declarative record 兩個抽屜,分別接住 var/函式與其餘宣告,同時持有 this 與 outer env)。每一層都是被上一層留下的缺口帶出來的,而不是憑空列舉。
結構鋪好後她才讓程式真的跑:creation phase 逐行登記宣告,execution phase 逐行賦值,呼叫函式時建立新的 function execution context,並讓它的 outer env 抄自 function object 的 environment 屬性。最後她做了整支影片最漂亮的一步收束——不另外開章解釋 hoisting、scope chain 與 closures,而是指出剛剛那一遍走查已經把三者演完了:hoisting 是 creation phase 的登記行為,scope chain 是沿 outer env 的查找路徑,closure 是 environment record 因為被 function object 握著而活得比建立它的 execution context 更久。三個常被當口訣背的名詞,就這樣被還原成同一組零件在不同時間點的三個側面。
推理鏈:每段留給下一段的線索
每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。
- 問題起點:什麼是 execution context → 問題被明確化了,但答案還沒出現:被推上 call stack 的那一格裡面到底裝了什麼?下一段要給出 execution context 的定義,以及它用來記帳的元件。
- 定義:執行環境與 environment record → 範例程式碼已經擺上桌,但一行都還沒開始跑。下一段要問:script 一被載入,第一個被建立的是什麼?它又會分成哪幾個階段?
- 兩個階段與 global EC 的三個元件 → 三格裡的第一格 realm 目前只是一條指標,指向一個 realm record。下一段要打開這個 record,看它究竟由哪些東西組成。
- Realm:隔離環境與 intrinsics → intrinsics 是實體,但我們平常是用 Array 這個名字去取用它。下一段要看 realm 的第二塊 global object,以及那些名字究竟是怎麼被掛上去的。
- Global object 的三種屬性來源 → global object 收下了 var 與全域函式宣告,但 const 與 let 顯然沒被收下——那它們的 binding 放在哪裡?下一段打開 realm 的第三塊 global environment record。
- Global environment record 的內部 → outer env 被標成「稍後很重要」但現在還是 null,先擱著。下一段要處理三格裡剩下的 lexical environment 與 variable environment,順便解釋作者為什麼決定把圖畫得簡單一點。
- Lexical 與 variable environment 的分工與簡化 → 框架與畫圖約定都備齊了,但範例程式碼一行都還沒被處理。下一段開始真的走 creation phase,逐行看三個宣告分別被送進哪一個 record。
- Creation phase 走一遍 script → creation phase 掃完了:declarative record 裡兩個名字還空著,function object 的 call 也還沒被觸發。下一段推上 call stack,看這些空格何時被填上,以及呼叫函式會生出什麼東西。
- Execution phase 與函式呼叫建立新 EC → 新建的 function environment record 裡只有 nameToGreet 和一個空的 fullName,可是第五行同時需要 lastName——它不在這裡。下一段就看引擎怎麼找到它,以及兩個 context 最後怎麼收場。
- 沿 scope chain 找變數,再逐一出堆疊 → 整段程式從載入到清空 call stack 已經完整走過一遍,而這一遍其實順手示範了三個平常被當口訣背的名詞。下一段回頭替其中的 hoisting 補上完整規則。
- Hoisting 的三種待遇與 TDZ → hoisting 已經結案,但剛剛那條沿 outer env 往外找的路徑、以及「函式記得住外層變數」這件事,都還沒被收成一句定義。下一段用同一組零件把它們收尾。
- Scope chain 與 closure 的同一條線
3. 逐段說明
JavaScript Visualized - Execution Contexts
1. 問題起點:什麼是 execution context 0:00–0:15
作者從最熟悉的現象切入:每次載入 script 或呼叫函式,就會建立一個新的 execution context 並推上 call stack,而 call stack 其實就是一個 execution context 的堆疊。但「execution context 到底是什麼」這件事本身沒被回答,這就是整支影片要拆解的問題。
推理因為前情提要只確立了「有東西被推上去」這個現象,所以作者接著把 call stack 本身拆開,指出堆在上面的每一格就是一個 execution context——call stack 不過是 execution context 的堆疊——然後立刻反問「那 execution context 究竟是什麼」,把整支影片的目標定成回答這個一直被跳過的問題。
AI 補充多數教學把 call stack 畫成一疊方塊就結束了,方塊裡裝什麼從來沒交代。作者在這裡做的事是把問題往下推一層:call stack 不是一個獨立的機制,它只是容器;要理解執行順序,得先理解被堆進去的那個東西。另外值得先建立一個預期——execution context 是規格層級的內部結構,你在程式碼裡看不到它、也拿不到它的參照,只能從行為(誰看得到誰、什麼時候能用、什麼時候報錯)反推它的形狀。這也是為什麼這支影片必須用畫的。
2. 定義:執行環境與 environment record 0:15–0:47
execution context 定義了程式碼執行時所處的環境,內含許多引擎用來追蹤執行流程的內部元件。其中它用 environment record 來保存與維護 identifier binding,也就是變數宣告、函式宣告與這個環境裡的所有值。作者接著宣布要用一段小 script 當例子,實際走一遍背後發生的事。

推理因為上一段已經把問題收斂成「execution context 是什麼」,所以作者接著給出兩句定義:它定義了程式碼執行時所處的環境,裡面有許多引擎用來追蹤執行流程的內部元件;而它用 environment record 來保存與維護 identifier binding,也就是變數宣告、函式宣告以及這個環境裡的所有值。定義給完,他馬上宣布不再堆抽象描述,改用一段九行的 script 當例子,實際走一遍背後發生的事。
AI 補充這裡有一個非常容易滑過去、但決定後面所有畫面怎麼看的分工:execution context 本身不存變數,它只持有指標;真正存放「名字 → 值」的是 environment record。之後畫面上每一個叫做「某某 environment」的欄位,本質都只是指向某個 environment record 的一條線。作者選擇用範例推進而不是逐條列規格,也有結構上的理由——execution context 的元件彼此互指,單獨定義任何一個都會用到另一個還沒定義的概念;只有沿著一段程式碼的執行順序走,這些指標才會一個一個地「因為被需要」而出現。畫面上的範例只有九行:兩個變數宣告(const firstName = "Lydia"、let lastName = "Hallie")、一個帶參數的函式宣告 greet(nameToGreet),最後一行呼叫 greet(firstName)。這九行接下來會被反覆引用,值得先記住。
3. 兩個階段與 global EC 的三個元件 0:47–1:26
script 載入時先建立 global execution context,而每個 execution context 都會經過兩個階段:creation phase 先為變數宣告、函式宣告配置記憶體,execution phase 才被推上 call stack 真正執行程式。作者聚焦 creation phase,並把 global EC 收斂成三個要看的元件:realm、lexical environment、variable environment。


推理因為上一段已經把範例程式碼擺好,所以作者接著讓時間軸開始走:script 載入時先建立 global execution context,而任何 execution context 都會經過兩個階段——creation phase 先為變數宣告、函式宣告配置記憶體,execution phase 才被推上 call stack、程式碼真正被執行。定下這個框架後,他把 global EC 眾多元件收斂成三格,宣布這一輪只看 realm、lexical environment 與 variable environment,其餘暫時灰掉。
AI 補充兩階段是這支影片槓桿最大的一個框架:之後所有「為什麼可以在宣告之前呼叫函式」「為什麼 const 提前存取會報錯」的答案,都只是在問同一句話——這件事發生在哪個階段。有一個作者沒明說的邊界值得補上:creation phase 並不是把整份 script 掃過一遍,它只處理當前這一層的宣告;函式體裡的宣告要等那個函式真的被呼叫、建立自己的 execution context 時才會被處理。另外,把元件從一長串收斂成三格是刻意的裁剪,畫面上灰掉的格子(例如 code evaluation state、function、script or module)在規格裡依然存在,只是與這次的推導無關;看圖時要能分辨「不存在」與「這次不看」。
4. Realm:隔離環境與 intrinsics 1:26–2:03
realm 是程式執行所在的隔離環境:瀏覽器裡開新分頁、重新整理、service worker、web worker、iframe 各自都是一個新的 realm。一個 realm 由 intrinsics、global object、global environment record 組成。intrinsics 提供 Array、Function、SyntaxError 這類執行 JavaScript 必備的標準內建物件與函式。

推理因為上一段把 realm 列為要看的第一個元件,所以作者接著先回答「realm 是什麼」——一個彼此隔離的執行環境;在瀏覽器裡,開新分頁、重新整理頁面、service worker、web worker、iframe,各自都會建立一個新的 realm。定義給完他立刻把 realm record 拆成三塊:intrinsics、global object、global environment record,並先講最底層的 intrinsics:它提供 Array、Function、SyntaxError 這類執行 JavaScript 必備的標準內建物件與函式。
- CRealm:彼此隔離的執行環境,各自有自己的 intrinsics、global object 與 global environment record→ 畫地圖
- R重新賦值 Array 為什麼不會壞掉→ 存+回想
- Aglobal object 上的 Array 只是名牌,實體住在 intrinsics 裡→ 批判類比
AI 補充「隔離」這個詞在這裡有非常具體的後果:兩個 realm 各有一整套自己的 intrinsics,所以在 iframe 裡建立的陣列,它的原型鏈接的是 iframe 那一份 Array.prototype,於是在外層做 arr instanceof Array 會得到 false,而 Array.isArray(arr) 仍然是 true——後者存在的理由正是跨 realm 判斷。畫面上 intrinsics 每一格都寫成 %Array%、%Function% 這種百分號包起來的形式,那是 ECMAScript 規格用來指稱「這個 realm 的那一份內建物件」的記法,和你在程式裡打的全域名字是兩回事:名字掛在 global object 上,實體住在 intrinsics 裡。這個分工也解釋了一件事——把 Array 重新賦值成別的東西,只是改了 global object 上的一個屬性,引擎內部要用陣列時仍然走 intrinsics,所以不會壞掉。
5. Global object 的三種屬性來源 2:03–2:50
global object 上的屬性分成三類:spec-defined 屬性把 intrinsics 曝露出來(Array、Function 等)、host-defined 屬性由執行環境提供(瀏覽器的 fetch、setTimeout、document)、user-defined 屬性則來自我們自己。後者可以明確掛上去,也會在全域宣告函式或用 var 宣告變數時隱式加入,之後整份 script 都能用。

推理因為上一段已經把 intrinsics 定位成「實體」,所以作者接著講 global object,並把它上面的屬性依來源分成三類:spec-defined 屬性把 intrinsics 曝露出來(Array、Function、Promise 這些);host-defined 屬性由執行環境提供,在瀏覽器就是 fetch、setTimeout、document;user-defined 屬性則來自我們自己,可以明確掛上去,也會在全域宣告函式或用 var 宣告變數時被隱式加入,之後整份 script 都能直接用。
AI 補充這個三分法真正的用處是解釋「為什麼有些東西 Node 有、瀏覽器沒有」:spec 屬性是規格要求每個 JavaScript 環境都要有的,host 屬性則是宿主自己加的,所以 document 在 Node 裡不存在不是缺陷,它本來就不屬於語言。第三類的「隱式加入」是本段後果最大的一句:var 宣告的變數與函式宣告在全域會成為 global object 的屬性,而 const、let、class 不會——這個分裂會在下一段直接被用來解釋「為什麼同一個環境需要兩個不同的 record」。要補上作者跳過的一步:這條隱式規則只適用於傳統的 classic script;ES module 的頂層宣告(包括 var 與 function)不會掛上 global object,這在今天的專案裡反而是常態。
術語:host-defined property
「we do it implicitly whenever we declare a function in the global scope or whenever we have a variable with VAR keyword in the global scope these are also added to the global object」
6. Global environment record 的內部 2:50–3:56
global environment record 管理整份 script 都能存取的 identifier binding,它內含 object record(直接指向 global object,服務 var 與函式宣告)與 declarative record(存放除這兩者以外的所有 binding)。它同時保存 this 的值——在全域就是 globalThis,多半指向 global object——以及 outer env 屬性,在全域環境是 null。作者特別預告 outer env 之後講 scope chain 時會變得關鍵。

推理因為上一段發現 global object 只收得下 var 與全域函式宣告,所以作者接著打開 global environment record,指出它內部同時掛著兩個 record:object record 是對 global object 的直接參照,服務 var 變數與函式宣告;declarative record 則存放除這兩者以外的所有 binding。他順帶交代同一個 record 還保存 this 的值——在全域就是 globalThis,多數情況下指向 global object——以及一個 outer env 屬性,在全域環境是 null,並特別預告這個屬性稍後講到 scope chain 時會變得非常關鍵。
- CGlobal environment record:內含 object record(收 var)與 declarative record(收其餘)→ 畫地圖
- Rconst 宣告的名字在 globalThis 上找不到→ 存+回想
- COuter env:指向外一層 environment record 的屬性,來源是宣告位置,全域時是 null→ 畫地圖
AI 補充「一個環境、兩個抽屜」正是上一段那個分裂的實作方式:查一個名字時,global environment record 會先問 declarative record,再問 object record。這就直接解釋了兩個常被當成怪癖的行為——宣告 const x 之後,globalThis.x 是 undefined;而宣告 var y 之後,globalThis.y 拿得到值。outer env 在全域被設成 null 看起來像廢話,但它其實是遞迴查找的終止條件:往外找 binding 的過程一路沿 outer env 前進,走到 null 還沒找到就停止並丟出錯誤,這一點稍後會被實際用到。至於 this,作者說「多數情況下指向 global object」是有意保留的:在瀏覽器裡 globalThis 實際上是 WindowProxy 而非 Window 本身,而 ES module 的頂層 this 是 undefined,都不是全域環境那個 this 的典型情況。
7. Lexical 與 variable environment 的分工與簡化 3:56–4:34
lexical environment 指向存放「var 以外」所有 binding 的 environment record,variable environment 則指向存放 var 變數的那一個;在全域這兩者都指向同一個 global environment record。作者坦言這樣畫太雜,宣布之後把它們合併成一個 global environment record 來看。這是一次刻意的簡化,後面的圖都採用這個約定。
推理因為上一段已經把 global environment record 的內部完全拆開,所以作者接著回頭補完 global EC 的另外兩格:lexical environment 指向存放「var 以外」所有 binding 的 environment record,variable environment 則指向存放 var 變數 binding 的那一個;而在全域,這兩條指標指到的是同一個 global environment record。講完他坦言這樣畫在視覺上太雜,宣布影片後半把它們合併成一個 global environment record 來畫。
AI 補充這兩格為什麼要分成兩個,影片沒有說:因為在函式與區塊裡,它們可以指向不同的 record。var 的作用域是整個函式,const 與 let 的作用域是區塊;每進入一個區塊,引擎會為 lexical environment 換上一個新的 environment record,而 variable environment 維持指向函式那一層——這正是為什麼在 if 區塊裡宣告的 var 會洩漏到區塊之外,而 let 不會。全域之所以兩者重合,只是因為外面沒有更大的區塊可以分開。另外要記得:從這裡開始的每一張圖都採用了合併後的畫法,那是為了看得懂而做的簡化,不是事實——規格上這兩個欄位始終是兩條各自獨立的指標。看圖時把它讀成「此處兩者恰好同指一處」,之後遇到函式那一層才不會混亂。
8. Creation phase 走一遍 script 4:34–6:17
在 creation phase 逐行掃描:const firstName 經 lexical environment 進到 declarative record,被 hoist 但 uninitialized;let lastName 同理。函式宣告 greet 則由 object record 管理,而且在 creation phase 就完成初始化,產生一個 function object,其中 environment 屬性指向宣告它的 global environment record,call 方法在被呼叫時執行。


推理因為上一段確立了「lexical environment → global environment record → 兩個 record」這條路徑,所以作者接著讓每一行宣告沿著這條路徑走一次:第一行 const firstName 走 lexical environment 進到 declarative record,記憶體配置好但保持 uninitialized;第二行 let lastName 完全同理;第四行的函式宣告 greet 則改由 object record 管理,而且在 creation phase 就完成初始化,產生一個 function object,其中 environment 屬性指向它被宣告時所在的 global environment record,call 方法則留待被呼叫時才執行。掃完沒有其他宣告,這一輪 creation phase 就結束了。
AI 補充這一段其實已經把 hoisting 整個演完了,只是還沒替它取名字:三個宣告都在 creation phase 被登記進 environment record,差別只在登記完之後手上有沒有值。這裡要特別分辨 uninitialized 與 undefined——前者是引擎內部才看得到的第三種狀態,讀到它的反應是丟出錯誤,而不是回傳一個值;把它想成「格子存在但貼著封條」比想成「格子裡是 undefined」準確得多。function object 上的 environment 屬性尤其值得停下來看:它在函式被建立的那一刻就固定指向宣告處的環境,之後不管這個函式被傳到哪裡、被誰呼叫,這條指標都不會變。這是 JavaScript 採用語彙作用域而非動態作用域的具體實作,也是稍後兩個名詞能夠成立的物質基礎。
9. Execution phase 與函式呼叫建立新 EC 6:17–7:50
global EC 被推上 call stack 開始執行:firstName 與 lastName 這時才被賦值,greet 已在記憶體中所以不做事。第九行呼叫函式時,function object 的 call 被觸發,建立新的 function execution context,同樣經過兩個階段;它的 lexical environment 是全新的 function environment record,outer env 指向 function object 的 environment,也就是 global environment record。參數在 creation phase 就立即以傳入值初始化,函式內的 const fullName 則仍是 uninitialized。

推理因為上一段已經把記憶體全部配置好了,所以作者接著把 global execution context 推上 call stack,進入 execution phase:第一行 firstName 這時才真正拿到字串 Lydia,第二行 lastName 拿到 Hallie,第四行的 greet 因為已經在記憶體裡所以什麼都不做;跑到第九行呼叫函式時,function object 上的 call 被觸發,建立一個全新的 function execution context,而它同樣要走 creation phase 與 execution phase 兩個階段。它的 lexical environment 指向一個全新的 function environment record,這個 record 的 outer env 抄自 function object 的 environment 屬性,也就是 global environment record;參數 nameToGreet 在 creation phase 就立刻以傳入的值初始化,而函式內的 const fullName 仍然是 uninitialized。
- Rfunction object 的 environment 屬性建立後就固定→ 存+回想
- R參數跟函式內 const 在 creation phase 待遇不同→ 存+回想
- P自己標出 creation phase 結束時每個 binding 的狀態→ 練習
AI 補充參數與 const 在同一個 creation phase 得到不同待遇,是這一段最容易被跳過的細節,但理由很直接:參數的值來自呼叫端,在建立這個 context 時就已經備妥,不必等執行到任何一行;fullName 的值要靠執行第五行的字串運算才算得出來,只能先留空。另一件必須看清楚的是 outer env 的來源——它抄的是 function object 的 environment,也就是函式被「宣告」的位置,而不是被「呼叫」的位置。這裡是整支影片真正的樞紐:如果 outer env 抄的是呼叫者的環境,JavaScript 就會變成動態作用域,接下來要講的查找與閉包兩件事都不會成立。順帶一提,自動字幕在這一段把 Hallie 聽成了 Hi,畫面上的值始終是 Hallie。
10. 沿 scope chain 找變數,再逐一出堆疊 7:50–9:00
進入函式的 execution phase,計算 fullName 時同時需要參數與 lastName,但 function environment record 沒有 lastName 的 binding,於是沿 outer env 往外查找——這就是 scope chain——在 global environment record 找到值 Hallie。函式回傳後,function execution context 從 call stack 移除,最上層回到 global EC;script 沒有別的事,global EC 也被移除,執行結束。

推理因為上一段已經把 outer env 指回了 global environment record,所以作者接著讓引擎沿著這條指標走:function environment record 找不到 lastName,就改用 outer env 往外一層找,而這一串環境串起來的路徑就叫 scope chain;在 global environment record 裡找到了 Hallie,於是 fullName 成為 Lydia Hallie,函式回傳 Hello, Lydia Hallie。回傳的同時,function execution context 從 call stack 被移除,最上層回到 global execution context;script 已經沒有別的事要做,global execution context 也被移除,整段程式結束。
AI 補充查找是沿著 outer env 一層一層往外走,不是「往下翻 call stack」——這兩件事在本例中碰巧一致(呼叫發生在全域),所以最容易在這裡被混為一談。把 greet 搬進另一個函式裡再呼叫,call stack 會多一層,scope chain 卻完全不變,因為 outer env 早在函式被宣告時就寫定了(見第 9 段)。查找失敗的結局也由這條鏈決定:一路走到 global environment record 的 outer env(也就是 null)仍沒找到,就是 ReferenceError。至於收尾這一步看似平凡,其實是後面閉包那一段的對照組:這裡 function environment record 沒有任何人參照,隨著 execution context 一起消失;等到有人握著它時,結局就會不同。
11. Hoisting 的三種待遇與 TDZ 9:00–10:14
作者回頭指出剛剛的走查已經順帶解釋了 hoisting:const、let、class、import 在 creation phase 被配置記憶體但保持 uninitialized,要到 execution phase 跑到宣告那行才初始化,提前存取就是 reference error,這段區間叫 temporal dead zone。var 同樣被 hoist,但初始化成 undefined,執行到宣告時才換成真值。函式(含 async、generator)在 creation phase 就以完整的 function object 初始化,所以可以在宣告之前呼叫。

推理因為上一段完成了一次從頭到尾的執行,所以作者接著回頭把 hoisting 定位成「發生在 creation phase 的事」,並依宣告種類分成三種待遇:const、let、class 與 import 被配置記憶體但保持 uninitialized,要到 execution phase 跑到真正的宣告那一行才初始化,提前存取就得到熟悉的 ReferenceError,而從區塊開始到宣告為止的這段區間叫做 temporal dead zone;var 同樣被 hoist,但會被初始化成 undefined,跑到宣告處才換成真值;函式(含 async function、generator function)在 creation phase 就已經用完整的 function object 初始化,所以可以在宣告之前呼叫。
- CHoisting:宣告在 creation phase 就被登記,依種類分三種待遇——空、undefined、或完整函式物件→ 畫地圖
- CTDZ:從區塊開始到 const/let 宣告那一行為止,存取該 binding 會得到 ReferenceError→ 畫地圖
- Rvar 的 undefined 比 TDZ 更危險→ 存+回想
AI 補充三種待遇的差別其實只有一個變數:creation phase 結束時那個 binding 手上拿到什麼——什麼都沒有(封條狀態)、undefined、還是一個完整的 function object。順著這個框架,兩個常見疑問也一起解決了。第一,temporal dead zone 的起點是所在區塊的開頭而不是檔案開頭,因為每進入一個區塊就換一個新的 environment record,死區自然也以區塊為單位。第二,在同一個區塊裡用 let 重複宣告同一個名字得到的是 SyntaxError 而不是 ReferenceError,那發生在更早的解析階段,和 hoisting 無關。另外值得補一句:var 的 undefined 之所以危險,正是因為它讓錯誤靜默——提前使用不會爆炸,只是拿到一個看似合理的值,錯誤要到很後面才浮現;const 與 let 的死區是刻意選擇讓它早點爆炸。
「classes and imports are hoisted so memory is allocated for them but they remain uninitialized」
12. Scope chain 與 closure 的同一條線 10:14–11:40
scope chain 就是 environment record 上 outer env 所提供的機制:找不到的識別字就沿著環境鏈往外找到為止。closure 則是內層函式保留了外層 environment record 的參照而形成,靠的是 function object 的 environment 屬性。把範例改成回傳 inner 函式本身後,呼叫時新建的 function environment record 其 outer env 指向外層函式的 environment record,因此 inner 仍能取到 lastName。作者最後預告會另拍影片細講 scope、hoisting 與 closures。

推理因為上一段已經用 creation phase 解釋完 hoisting,所以作者接著用完全相同的零件解釋剩下兩個名詞:scope chain 就是 environment record 上 outer env 屬性所提供的機制——目前環境的 record 裡找不到的識別字,引擎就沿著環境鏈往外走,直到找到那個 binding 為止;而 closure 則是內層函式保留了外層函式 environment record 的參照時形成的,做得到這件事靠的是 function object 上的 environment 屬性。他把範例改寫成 outer 直接回傳 inner 這個 function object 本身(而不是回傳 inner 的執行結果),於是呼叫 getFullName 時新建的 function environment record,其 outer env 指向 inner 的 environment,也就是 outer 的 environment record,因此在 inner 裡仍然取得到 lastName。最後他預告會另拍一支影片細講 scope、hoisting 與 closures。
- CClosure:內層函式透過 function object 的 environment 屬性,保留外層 environment record 的參照→ 畫地圖
- Rclosure 保留的是參照不是複製→ 存+回想
AI 補充改寫後的關鍵變化其實只有一個:outer 執行完畢、它的 function execution context 已經離開 call stack,但它的 environment record 沒有被回收——因為 inner 的 function object 還握著指向它的 environment 屬性。所以「closure 把變數關起來」這個說法可以講得更精確:closure 沒有複製任何值,它只是讓一個 environment record 的存活期超過建立它的那個 execution context。兩個推論隨之而來。第一,同一次呼叫 outer 所產生的多個內層函式共用同一份 record,因此彼此看得到對方的修改;第二,每次重新呼叫 outer 都會產生一份新的 record,所以用閉包做計數器時,每次呼叫工廠函式都會從頭算起。到這裡整支影片閉合了:開場那個「call stack 上堆的到底是什麼」,答案是一疊各自持有 realm、環境指標與 outer env 的 execution context;而 hoisting、scope chain、closure 三個名詞,只是這同一疊結構在不同時間點呈現出來的三個側面。
4. 總結
作者從一個熟悉的現象出發:載入 script 或呼叫函式就會有新的 execution context 被推上 call stack——但那一格裡裝了什麼從沒被回答。他先給定義(execution context 是程式碼執行所在的環境,用 environment record 記帳),再定下兩個階段的框架(creation phase 配置記憶體、execution phase 才真的執行),然後打開 global execution context 的三格:realm 指向隔離環境,內含 intrinsics、global object 與 global environment record;global object 上的名字分成規格定義、宿主定義、使用者定義三種來源;global environment record 內部又分成直通 global object 的 object record(收 var 與函式宣告)與 declarative record(收 const / let),並帶著 this 與此刻為 null 的 outer env。接著把範例 script 放進這套框架走一遍:creation phase 裡 const / let 被配置但 uninitialized,函式則已經生成完整的 function object(帶 environment 與 call);execution phase 賦值、呼叫函式、生出新的 function execution context,其 outer env 指回宣告處的環境。函式需要 lastName 而自己沒有,於是沿 outer env 往外找——這條路徑就是 scope chain;找到後回傳、兩個 context 依序離開 call stack。最後作者回頭指出:剛剛那一遍已經示範完 hoisting(三種宣告在 creation phase 拿到三種待遇,const / let 落在 temporal dead zone)、scope chain(outer env 的查找機制)與 closure(內層函式透過 function object 的 environment 屬性抓住外層 environment record)。三個常被當口訣背的名詞,其實是同一組零件在不同時刻的樣子。
勘誤總整理
確定錯誤/已過時見仁見智(取決於版本或情境)
| 段落 | 原話(transcript 逐字) | 說明 |
|---|---|---|
| 5. Global object 的三種屬性來源 2:35 |
「we do it implicitly whenever we declare a function in the global scope or whenever we have a variable with VAR keyword in the global scope these are also added to the global object」 | 這句只在 classic script 成立。若這段程式是以 ES module 載入(<script type="module">、Node 的 .mjs 或 type: module),頂層的 var 與 function 宣告會進入 module environment record,不會成為 global object 的屬性,因此 globalThis 上取不到它們。今日多數專案以 module 為預設,作者未區分這兩種載入方式。 依據: ECMAScript:GlobalDeclarationInstantiation(script)對比 InitializeEnvironment(module) |
| 11. Hoisting 的三種待遇與 TDZ 9:16 |
「classes and imports are hoisted so memory is allocated for them but they remain uninitialized」 | 把 import 和 const/let/class 歸為同一種待遇並不精確。module 的 import binding 在連結(linking)階段就完成初始化,早於整個 module 的求值,所以在 import 敘述之前的位置使用被匯入的名字是合法的,並不落在 temporal dead zone。只有在循環相依的 module 圖中,提前存取另一個尚未求值完成的 module 匯出,才會真的得到 ReferenceError。 依據: ECMAScript 規格:module 的 InitializeEnvironment 於 Link 階段執行,早於 Evaluate |
5. 推薦三個下一步
1. 往下挖深:閉包與記憶體
影片只把 closure 定義成「內層函式抓住外層 environment record」,但沒談這條參照會讓多少東西留在記憶體裡、什麼時候才被回收——這正是實務上 memory leak 的來源。
YouTube 搜尋:javascript closures memory leak v8 garbage collection closures javascript visualized scope chain
2. 往旁邊對照:event loop 與 call stack 的另一半
本片把 call stack 走完就結束了,但真實程式裡 setTimeout、Promise 會讓 execution context 不是連續地上下堆疊。補上 event loop 才看得到完整的執行流程。
YouTube 搜尋:javascript visualized event loop microtask queue macrotask javascript promise execution
3. 往上應用:用 debugger 對照真實的 scope
影片畫的 environment record、outer env 在 Chrome DevTools 的 Scope 面板上有一一對應的欄位。親手在中斷點上看一次 Local / Closure / Global,這套抽象才會變成可驗證的東西。
YouTube 搜尋:chrome devtools scope panel closure debug javascript call stack devtools temporal dead zone devtools
- 接著看 ←9 JavaScript Concepts That Got Me To Senior Dev · 那站把 call stack、closure、scope chain 列成清單,這站還原成同一組 execution context 零件
- 接著看 →JavaScript Visualized - Closures · 共用 9 個術語;closure 在這站只是一條 environment 指標,那站專門把它講到底
- 接著看 →JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue · 這站走完 call stack 就結束,那站補上 web API 與兩個佇列讓順序不再連續
- 接著看 →JavaScript Visualized - Promise Execution · execution context 與 call stack 建立後,那站看 Promise 怎麼排進微任務佇列
- 接著看 →Senior Frontend Interview 2026 🎉 | Javascript 🎯 (Mock) [Most Asked Questions] · closure、scope chain、hoisting 在這站是機制,在那場模擬面試裡是被驗收的答案
- 相關 —Reactivity in Vue 3 - How does it work? · 共用 closure:一站是語言機制本身,一站用它做響應式的依賴追蹤