JavaScript execution context:從 call stack 上的一格,拆到 realm、environment record、scope chain 與 closure

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

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

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

1. Outline

  1. 起點 · 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 更久。三個常被當口訣背的名詞,就這樣被還原成同一組零件在不同時間點的三個側面。

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

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

  1. 問題起點:什麼是 execution context → 問題被明確化了,但答案還沒出現:被推上 call stack 的那一格裡面到底裝了什麼?下一段要給出 execution context 的定義,以及它用來記帳的元件。
  2. 定義:執行環境與 environment record → 範例程式碼已經擺上桌,但一行都還沒開始跑。下一段要問:script 一被載入,第一個被建立的是什麼?它又會分成哪幾個階段?
  3. 兩個階段與 global EC 的三個元件 → 三格裡的第一格 realm 目前只是一條指標,指向一個 realm record。下一段要打開這個 record,看它究竟由哪些東西組成。
  4. Realm:隔離環境與 intrinsics → intrinsics 是實體,但我們平常是用 Array 這個名字去取用它。下一段要看 realm 的第二塊 global object,以及那些名字究竟是怎麼被掛上去的。
  5. Global object 的三種屬性來源 → global object 收下了 var 與全域函式宣告,但 const 與 let 顯然沒被收下——那它們的 binding 放在哪裡?下一段打開 realm 的第三塊 global environment record。
  6. Global environment record 的內部 → outer env 被標成「稍後很重要」但現在還是 null,先擱著。下一段要處理三格裡剩下的 lexical environment 與 variable environment,順便解釋作者為什麼決定把圖畫得簡單一點。
  7. Lexical 與 variable environment 的分工與簡化 → 框架與畫圖約定都備齊了,但範例程式碼一行都還沒被處理。下一段開始真的走 creation phase,逐行看三個宣告分別被送進哪一個 record。
  8. Creation phase 走一遍 script → creation phase 掃完了:declarative record 裡兩個名字還空著,function object 的 call 也還沒被觸發。下一段推上 call stack,看這些空格何時被填上,以及呼叫函式會生出什麼東西。
  9. Execution phase 與函式呼叫建立新 EC → 新建的 function environment record 裡只有 nameToGreet 和一個空的 fullName,可是第五行同時需要 lastName——它不在這裡。下一段就看引擎怎麼找到它,以及兩個 context 最後怎麼收場。
  10. 沿 scope chain 找變數,再逐一出堆疊 → 整段程式從載入到清空 call stack 已經完整走過一遍,而這一遍其實順手示範了三個平常被當口訣背的名詞。下一段回頭替其中的 hoisting 補上完整規則。
  11. Hoisting 的三種待遇與 TDZ → hoisting 已經結案,但剛剛那條沿 outer env 往外找的路徑、以及「函式記得住外層變數」這件事,都還沒被收成一句定義。下一段用同一組零件把它們收尾。
  12. 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 到底是什麼」這件事本身沒被回答,這就是整支影片要拆解的問題。

承上 前情提要裡「載入 script 或呼叫函式,就會有東西被推上 call stack」是大多數寫過 JavaScript 的人已經有的印象;作者把這個印象當成整支影片唯一的已知條件,從它出發。

推理因為前情提要只確立了「有東西被推上去」這個現象,所以作者接著把 call stack 本身拆開,指出堆在上面的每一格就是一個 execution context——call stack 不過是 execution context 的堆疊——然後立刻反問「那 execution context 究竟是什麼」,把整支影片的目標定成回答這個一直被跳過的問題。

AI 補充多數教學把 call stack 畫成一疊方塊就結束了,方塊裡裝什麼從來沒交代。作者在這裡做的事是把問題往下推一層:call stack 不是一個獨立的機制,它只是容器;要理解執行順序,得先理解被堆進去的那個東西。另外值得先建立一個預期——execution context 是規格層級的內部結構,你在程式碼裡看不到它、也拿不到它的參照,只能從行為(誰看得到誰、什麼時候能用、什麼時候報錯)反推它的形狀。這也是為什麼這支影片必須用畫的。

execution context

執行環境(執行脈絡)

引擎每次要執行一段程式碼時建立的內部結構,定義了這段程式碼執行時所處的環境。

它是 ECMAScript 規格裡的概念,不是語言暴露給開發者的物件,所以你無法在程式中取得或列印它。每個 execution context 都持有數個內部欄位(例如 realm、lexical environment、variable environment),這些欄位大多只是指標,指向真正存資料的結構。常見誤解是把它和「作用域」畫上等號:作用域是查找名字的規則,execution context 則是承載這些規則所需結構的那一格;同一個函式被呼叫兩次會有兩個 execution context,但它們的作用域關係完全相同。

相關術語: call stack (被堆進)、environment record (持有其指標)

出處:第 1 段「問題起點:什麼是 execution context」

call stack

呼叫堆疊

由 execution context 一層層疊起來的結構,最上層就是目前正在執行的那一個。

它是後進先出的:新的 execution context 被推上去,執行結束就被移除,控制權回到下面那一層。瀏覽器開發者工具裡看到的呼叫堆疊、錯誤訊息裡的 stack trace,都是它的投影。要特別注意 call stack 反映的是「誰呼叫了誰」,也就是執行時的動態關係;它和名字的查找路徑不是同一回事,後者由宣告位置決定。

相關術語: execution context (由其堆疊而成)

出處:第 1 段「問題起點:什麼是 execution context」

留給下一段 問題被明確化了,但答案還沒出現:被推上 call stack 的那一格裡面到底裝了什麼?下一段要給出 execution context 的定義,以及它用來記帳的元件。

2. 定義:執行環境與 environment record 0:15–0:47

execution context 定義了程式碼執行時所處的環境,內含許多引擎用來追蹤執行流程的內部元件。其中它用 environment record 來保存與維護 identifier binding,也就是變數宣告、函式宣告與這個環境裡的所有值。作者接著宣布要用一段小 script 當例子,實際走一遍背後發生的事。

整支影片的參照範例程式碼(const firstName / let lastName / function greet),後面每一步都在這段 code 上推進,沒看到它就無法對照。
0:43 · 整支影片的參照範例程式碼(const firstName / let lastName / function greet),後面每一步都在這段 code 上推進,沒看到它就無法對照。
承上 承接上一段留下的問題——被推上 call stack 的那一格裡裝了什麼——作者直接給出定義,不再繞路。

推理因為上一段已經把問題收斂成「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)。這九行接下來會被反覆引用,值得先記住。

environment record

環境紀錄

真正保存「識別字 → 值」對應的內部結構,execution context 透過它來管理這個環境裡的所有 binding。

規格裡它是一個抽象型別,底下有多種具體形式(全域的、函式的、物件式的、宣告式的、模組的),差別在於 binding 存在哪裡、以及有沒有額外欄位。它同時還記錄 this 的值與指向外層環境的指標,所以它不只是一張表,也是作用域鏈上的一個節點。把它和 execution context 分開理解很重要:execution context 會隨著函式返回而消失,environment record 卻可能因為還被別人參照而繼續存活。

相關術語: identifier binding (存放)、execution context (被其指向)

出處:第 2 段「定義:執行環境與 environment record」

identifier binding

識別字綁定

一個名字與它所指的值之間的對應關係,是 environment record 記帳的最小單位。

「宣告一個變數」在規格裡其實是兩件分開的事:建立 binding(把名字登記進 environment record)與初始化 binding(給它值)。這兩件事會落在不同的時間點,正是後面各種提升行為的根源。binding 還帶有可不可變的屬性,const 建立的是不可重新賦值的 binding——注意是 binding 不可變,不是值不可變,所以 const 宣告的物件內容仍然可以改。

相關術語: environment record (被其管理)

出處:第 2 段「定義:執行環境與 environment record」

留給下一段 範例程式碼已經擺上桌,但一行都還沒開始跑。下一段要問:script 一被載入,第一個被建立的是什麼?它又會分成哪幾個階段?

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。

creation phase / execution phase 兩階段的對照圖,是全片反覆套用的框架。
0:57 · creation phase / execution phase 兩階段的對照圖,是全片反覆套用的框架。
global execution context 的元件盤,標出這次只看 realm、lexical environment、variable environment 三格。
1:21 · global execution context 的元件盤,標出這次只看 realm、lexical environment、variable environment 三格。
承上 上一段留下「script 載入後的第一步是什麼」,作者就從這一步開始按時間順序推。

推理因為上一段已經把範例程式碼擺好,所以作者接著讓時間軸開始走: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)在規格裡依然存在,只是與這次的推導無關;看圖時要能分辨「不存在」與「這次不看」。

creation phase

建立階段

execution context 的第一個階段:為當前這一層的變數與函式宣告配置記憶體,但還不執行程式碼。

在這個階段引擎會掃過這一層的宣告,把名字登記進對應的 environment record,並依宣告種類決定要不要順便給值。它是靜態的:不執行任何運算式,也不會進入函式體。理解它最實際的用處是——所有「還沒跑到那一行卻已經有效」的現象都發生在這裡,包括函式可以先呼叫後宣告、以及變數在宣告前存取時的各種不同結局。

相關術語: execution phase (對照組)、hoisting (發生於此)

出處:第 3 段「兩個階段與 global EC 的三個元件」

execution phase

執行階段

execution context 的第二個階段:被推上 call stack,程式碼逐行執行、變數在這時才拿到值。

creation phase 準備好的空格,在這個階段依程式碼順序被填上。差別在於這個階段是動態的:運算式被求值、函式被呼叫、新的 execution context 因此被建立。一個常見的混淆是把「被推上 call stack」當成 execution context 的誕生時刻,其實它在 creation phase 就已經存在,只是還沒開始跑。

相關術語: creation phase (接續其後)、call stack (此時被推上)

出處:第 3 段「兩個階段與 global EC 的三個元件」

global execution context

全域執行環境

script 一被載入就建立的最外層 execution context,位於 call stack 的最底層。

它和函式的 execution context 走一樣的兩階段流程,差別在於它沒有參數、沒有外層環境,而且它的環境指標最終都指向同一個 global environment record。它在整份 script 執行完畢後才被移除,所以只要頁面還在跑,它就一直在堆疊底部。

相關術語: execution context (一種)、realm (持有其指標)

出處:第 3 段「兩個階段與 global EC 的三個元件」

留給下一段 三格裡的第一格 realm 目前只是一條指標,指向一個 realm record。下一段要打開這個 record,看它究竟由哪些東西組成。

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 record 的組成圖(intrinsics / global object / global environment record),是接下來三段的目錄。
1:47 · realm record 的組成圖(intrinsics / global object / global environment record),是接下來三段的目錄。
承上 承上一段點名的第一格:realm 這條指標所指向的 realm record,裡面裝了什麼。

推理因為上一段把 realm 列為要看的第一個元件,所以作者接著先回答「realm 是什麼」——一個彼此隔離的執行環境;在瀏覽器裡,開新分頁、重新整理頁面、service worker、web worker、iframe,各自都會建立一個新的 realm。定義給完他立刻把 realm record 拆成三塊:intrinsics、global object、global environment record,並先講最底層的 intrinsics:它提供 Array、Function、SyntaxError 這類執行 JavaScript 必備的標準內建物件與函式。

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,所以不會壞掉。

realm

領域(隔離執行環境)

一個彼此隔離的程式執行環境,擁有自己的 intrinsics、global object 與 global environment record。

每開一個分頁、重新整理一次頁面、每個 iframe、每個 worker,都是一個新的 realm。隔離的意思是內建物件不共用:跨 realm 傳遞的值,其原型來自來源 realm,這是許多「明明是陣列卻不是 Array」錯誤的根源。realm 也是安全邊界的基礎,瀏覽器的同源政策、以及 ShadowRealm 這類提案,都建立在這個概念上。

相關術語: intrinsics (由其組成)、global object (由其組成)

出處:第 4 段「Realm:隔離環境與 intrinsics」

intrinsics

內建物件本體

規格保證每個 realm 都具備的一整套標準內建物件與函式的實體,例如 Array、Function、SyntaxError。

規格用 %Array%、%Object.prototype% 這種百分號記法指稱它們,強調指的是「這個 realm 的那一份實體」而不是某個名字。引擎自己在丟錯誤、建立字面量陣列時直接使用 intrinsics,不經過全域名字,所以開發者覆寫全域變數不會影響語言本身的運作。可以把 intrinsics 理解成語言的骨架,global object 只是貼在骨架外面的名牌。

相關術語: global object (將其曝露為名字)、realm (隸屬於)

出處:第 4 段「Realm:隔離環境與 intrinsics」

留給下一段 intrinsics 是實體,但我們平常是用 Array 這個名字去取用它。下一段要看 realm 的第二塊 global object,以及那些名字究竟是怎麼被掛上去的。

5. Global object 的三種屬性來源 2:03–2:50

global object 上的屬性分成三類:spec-defined 屬性把 intrinsics 曝露出來(Array、Function 等)、host-defined 屬性由執行環境提供(瀏覽器的 fetch、setTimeout、document)、user-defined 屬性則來自我們自己。後者可以明確掛上去,也會在全域宣告函式或用 var 宣告變數時隱式加入,之後整份 script 都能用。

global object 三類屬性並列的圖,spec / host / user 的分界只有畫面上看得出來。
2:30 · global object 三類屬性並列的圖,spec / host / user 的分界只有畫面上看得出來。
承上 上一段留下「intrinsics 的實體要靠什麼名字才取得到」,答案就在 realm 的第二塊上。

推理因為上一段已經把 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

global object

全域物件

realm 中承載所有全域可見名字的物件,屬性依來源分成規格定義、宿主定義與使用者定義三類。

在瀏覽器它是 window,在 Node 是 global,而 globalThis 是不分環境都能取得它的標準寫法。它同時扮演兩個角色:既是內建物件的名冊,也是全域環境的一個儲存區。常見誤解是以為所有全域變數都在它上面,實際上只有 var 與函式宣告會,const 與 let 建立的全域 binding 存在另一個地方,用點記法取不到。

相關術語: intrinsics (曝露其實體)、object record (被其直接參照)

出處:第 5 段「Global object 的三種屬性來源」

host-defined property

宿主定義屬性

由執行環境(瀏覽器、Node、Deno)而非 JavaScript 規格提供的全域屬性,例如 fetch、setTimeout、document。

JavaScript 規格本身沒有任何 I/O、計時器或 DOM,這些能力全部由宿主注入 global object。這就是為什麼同一段語法在瀏覽器與 Node 跑起來能力不同,也是為什麼寫跨環境的程式庫要小心檢查這些名字是否存在。近年多個宿主逐漸對齊(例如 Node 也提供了 fetch),但那是各自實作趨同,不是規格保證。

相關術語: global object (掛載於)、intrinsics (對照組)

出處:第 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)
留給下一段 global object 收下了 var 與全域函式宣告,但 const 與 let 顯然沒被收下——那它們的 binding 放在哪裡?下一段打開 realm 的第三塊 global environment record。

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 時會變得關鍵。

object record 與 declarative record 並排的圖,說明 var/function 與 const/let 被分到不同容器,是後面 hoisting 差異的來源。
3:21 · object record 與 declarative record 並排的圖,說明 var/function 與 const/let 被分到不同容器,是後面 hoisting 差異的來源。
承上 上一段結尾留下的問題是「const、let 的 binding 不在 global object 上,那到底放哪」,這一段的 global environment record 正是答案。

推理因為上一段發現 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 時會變得非常關鍵。

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 的典型情況。

global environment record

全域環境紀錄

管理整份 script 都能存取的 binding 的 environment record,內部同時包含 object record 與 declarative record。

它是 realm 的三塊組成之一,也是全域這一層作用域的實體。它比一般 environment record 多一層結構,因為全域必須同時相容兩套歷史:早期靠 global object 承載的 var 與函式宣告,以及 ES2015 之後不再汙染 global object 的 const、let、class。查找名字時它先問宣告式的那一半,再問物件式的那一半。

相關術語: object record (包含)、declarative record (包含)

出處:第 6 段「Global environment record 的內部」

object record

物件式紀錄

global environment record 中直接指向 global object 的部分,負責 var 變數與函式宣告的 binding。

它不自己存資料,只是把對 global object 的屬性操作包裝成 binding 操作,所以在這裡建立的名字會同時出現在 globalThis 上。也因為底層是物件屬性,這些 binding 可以被 delete、可以被列舉,行為和一般變數不完全一樣。with 陳述式建立的環境也是這一類,那正是 with 被視為有害的原因之一。

相關術語: global object (直接參照)、declarative record (對照組)

出處:第 6 段「Global environment record 的內部」

declarative record

宣告式紀錄

global environment record 中存放 var 與函式宣告以外所有 binding 的部分,例如 const、let、class。

它把名字直接存在引擎內部結構裡,不經過任何可被程式碼觸及的物件,因此這些 binding 無法被 delete、也不會出現在 globalThis 上。這種設計讓 const 與 let 的行為更可預測,也讓引擎有機會做更好的最佳化。函式與區塊建立的環境使用的也是這一類 record。

相關術語: object record (並列於同一環境)

出處:第 6 段「Global environment record 的內部」

globalThis

全域 this 值

全域環境裡 this 的值,多數情況下指向 global object,也是跨環境取得 global object 的標準寫法。

在 globalThis 成為標準之前,取得全域物件要依環境分別寫 window、self、global,寫跨平台程式庫時相當麻煩。要注意兩個例外:ES module 的頂層 this 是 undefined 而不是 globalThis;而在瀏覽器裡 globalThis 取到的其實是 WindowProxy,它會隨頁面導覽轉指向新的 Window,所以它和 global object 並非永遠同一個物件。

相關術語: global object (多半指向)、global environment record (保存於)

出處:第 6 段「Global environment record 的內部」

outer env

外層環境指標

environment record 上指向外一層 environment record 的屬性,在全域環境是 null。

它是把一個個獨立的環境串成鏈的那條線,決定了名字找不到時要往哪裡找。關鍵在於這條線在環境被建立時就固定,來源是程式碼的書寫位置而非呼叫位置,這正是 JavaScript 採用語彙作用域的實作方式。全域的 outer env 是 null,等於是這條鏈的終點,也是查找失敗時報錯的地方。

相關術語: scope chain (構成)、environment record (屬性之一)

出處:第 6 段「Global environment record 的內部」

留給下一段 outer env 被標成「稍後很重要」但現在還是 null,先擱著。下一段要處理三格裡剩下的 lexical environment 與 variable environment,順便解釋作者為什麼決定把圖畫得簡單一點。

7. Lexical 與 variable environment 的分工與簡化 3:56–4:34

lexical environment 指向存放「var 以外」所有 binding 的 environment record,variable environment 則指向存放 var 變數的那一個;在全域這兩者都指向同一個 global environment record。作者坦言這樣畫太雜,宣布之後把它們合併成一個 global environment record 來看。這是一次刻意的簡化,後面的圖都採用這個約定。

承上 承上一段擱下的線索:realm 那一格已經講完了,global EC 還剩 lexical environment 與 variable environment 兩格沒有交代。

推理因為上一段已經把 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 不會。全域之所以兩者重合,只是因為外面沒有更大的區塊可以分開。另外要記得:從這裡開始的每一張圖都採用了合併後的畫法,那是為了看得懂而做的簡化,不是事實——規格上這兩個欄位始終是兩條各自獨立的指標。看圖時把它讀成「此處兩者恰好同指一處」,之後遇到函式那一層才不會混亂。

lexical environment

語彙環境

execution context 上的欄位,指向存放「var 以外」所有 binding 的 environment record。

名字裡的 lexical 指的是「由程式碼書寫位置決定」,也就是靜態作用域。它會隨著進入區塊而更換所指的 record,因此 const 與 let 才會有區塊作用域。要注意規格裡這個詞有兩種用法:一是 execution context 上的這個欄位,二是舊版規格中泛指環境本身;讀不同年份的資料時容易混淆。

相關術語: variable environment (對照組)、environment record (指向)

出處:第 7 段「Lexical 與 variable environment 的分工與簡化」

variable environment

變數環境

execution context 上的欄位,指向存放 var 宣告 binding 的 environment record。

它在整個函式執行期間固定不動,不隨區塊改變,這就是 var 具有函式作用域而非區塊作用域的實作原因。在全域 execution context 中,它與 lexical environment 指向同一個 global environment record,所以看不出差別;差別只有在函式內含區塊時才顯現。這也是為什麼多數風格指南建議完全放棄 var——它讓兩個欄位不一致,是最常見的作用域錯誤來源。

相關術語: lexical environment (並列欄位)、object record (在全域指向)

出處:第 7 段「Lexical 與 variable environment 的分工與簡化」

留給下一段 框架與畫圖約定都備齊了,但範例程式碼一行都還沒被處理。下一段開始真的走 creation phase,逐行看三個宣告分別被送進哪一個 record。

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 方法在被呼叫時執行。

const/let 被標成 uninitialized 的畫面,是 TDZ 的視覺根據。
5:08 · const/let 被標成 uninitialized 的畫面,是 TDZ 的視覺根據。
function object 的內部(environment 與 call 兩個屬性),closures 那段完全建立在這張圖上。
6:02 · function object 的內部(environment 與 call 兩個屬性),closures 那段完全建立在這張圖上。
承上 上一段留下「結構備齊、程式碼還沒動」,這一段就把那九行程式碼放進剛剛畫好的結構裡。

推理因為上一段確立了「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 採用語彙作用域而非動態作用域的具體實作,也是稍後兩個名詞能夠成立的物質基礎。

hoisting

提升

宣告在 creation phase 就被登記進 environment record,因而「早於」程式碼順序存在的現象。

把它說成「宣告被搬到檔案最上面」是一個會誤導人的比喻:程式碼一行都沒有移動,移動的是概念上的時間——登記與賦值被拆到兩個不同階段。因此 hoisting 不是一種行為,而是三種:什麼都不給(const、let、class)、給 undefined(var)、給完整的函式物件(函式宣告)。理解成「creation phase 結束時這個 binding 手上有什麼」比背口訣有用得多。

相關術語: creation phase (發生於此)、uninitialized (其結果之一)

出處:第 8 段「Creation phase 走一遍 script」

uninitialized

未初始化狀態

binding 已經建立但尚未被賦值的內部狀態,讀取它會丟出錯誤,與值為 undefined 不同。

規格用一個特殊標記表示這個狀態,它不是任何 JavaScript 值,所以你無法把它取出來比較。這解釋了一個常見困惑:為什麼 typeof 對未宣告的變數是安全的(回傳 "undefined"),對處於這個狀態的 const 或 let 卻會直接丟錯。可以把它理解成「格子已經預留、但被封住」,執行到宣告那一行才拆封。

相關術語: temporal dead zone (造成)、identifier binding (其狀態)

出處:第 8 段「Creation phase 走一遍 script」

function object

函式物件

函式宣告在 creation phase 產生的物件,帶有 environment(宣告處環境)與 call(被呼叫時執行)等內部屬性。

JavaScript 的函式是一等公民,因為它就是物件,可以被賦值、傳遞、當作回傳值。它的 environment 屬性在建立當下就綁定宣告位置的 environment record 且永不改變,這是語彙作用域的實作;call 屬性則封裝了「被呼叫時要做什麼」,包括建立新的 execution context。這兩個屬性一靜一動的分工,是後面所有推導的支點。

相關術語: environment record (透過 environment 指向)、closure (使其可能)

出處:第 8 段「Creation phase 走一遍 script」

留給下一段 creation phase 掃完了:declarative record 裡兩個名字還空著,function object 的 call 也還沒被觸發。下一段推上 call stack,看這些空格何時被填上,以及呼叫函式會生出什麼東西。

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。

function environment record 的 outer env 箭頭指回 global environment record,這條線就是後面 scope chain 與 closure 的實體。
7:21 · function environment record 的 outer env 箭頭指回 global environment record,這條線就是後面 scope chain 與 closure 的實體。
承上 承上一段留下的兩件未完成事:declarative record 裡的空格何時被填值,以及 function object 的 call 何時被觸發。

推理因為上一段已經把記憶體全部配置好了,所以作者接著把 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。

AI 補充參數與 const 在同一個 creation phase 得到不同待遇,是這一段最容易被跳過的細節,但理由很直接:參數的值來自呼叫端,在建立這個 context 時就已經備妥,不必等執行到任何一行;fullName 的值要靠執行第五行的字串運算才算得出來,只能先留空。另一件必須看清楚的是 outer env 的來源——它抄的是 function object 的 environment,也就是函式被「宣告」的位置,而不是被「呼叫」的位置。這裡是整支影片真正的樞紐:如果 outer env 抄的是呼叫者的環境,JavaScript 就會變成動態作用域,接下來要講的查找與閉包兩件事都不會成立。順帶一提,自動字幕在這一段把 Hallie 聽成了 Hi,畫面上的值始終是 Hallie。

function execution context

函式執行環境

呼叫函式時透過 function object 的 call 建立的 execution context,一樣經過建立與執行兩個階段。

每一次呼叫都會建立一個全新的實例,所以遞迴時 call stack 上會有多個同名函式的 execution context,彼此的 environment record 互不相干。它和 global execution context 的差別在於多了參數處理、多了 outer env 不為 null,以及函式返回時它會被立刻移除。要注意它被移除不代表它的 environment record 也消失。

相關術語: global execution context (對照組)、function object (由其 call 建立)

出處:第 9 段「Execution phase 與函式呼叫建立新 EC」

function environment record

函式環境紀錄

function execution context 專屬的 environment record,管理參數與函式內宣告的 binding,並帶有指向外層的 outer env。

它是宣告式 record 的一種,額外保存了 this 的綁定方式與(若有)new.target。它的 outer env 來自 function object 的 environment,因此固定指向宣告處的環境。箭頭函式的差別就在這裡:它不建立自己的 this 綁定,所以 this 會沿著同一條鏈往外找,這就是箭頭函式「繼承外層 this」的實際機制。

相關術語: outer env (帶有)、environment record (一種)

出處:第 9 段「Execution phase 與函式呼叫建立新 EC」

留給下一段 新建的 function environment record 裡只有 nameToGreet 和一個空的 fullName,可是第五行同時需要 lastName——它不在這裡。下一段就看引擎怎麼找到它,以及兩個 context 最後怎麼收場。

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 往外查找的動線圖,scope chain 這個抽象詞在這張圖上才變成一條看得見的路徑。
8:27 · 沿 outer env 往外查找的動線圖,scope chain 這個抽象詞在這張圖上才變成一條看得見的路徑。
承上 上一段留下的缺口是:第五行要用到 lastName,但新建的 function environment record 裡根本沒有這個 binding。

推理因為上一段已經把 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 一起消失;等到有人握著它時,結局就會不同。

scope chain

作用域鏈

沿著 environment record 的 outer env 一層層往外查找 binding 的機制。

它不是一個實際存在的資料結構,而是「反覆跟隨 outer env」這個行為的名字。因為 outer env 由宣告位置決定,所以作用域鏈是靜態的:讀原始碼就能推斷一個名字會找到哪裡,不必知道誰呼叫了誰。這也是為什麼把函式當參數傳來傳去,它看得到的變數集合不會改變。查找到 null 仍未命中就丟出錯誤。

相關術語: outer env (由其構成)、call stack (常被混淆)

出處:第 10 段「沿 scope chain 找變數,再逐一出堆疊」

留給下一段 整段程式從載入到清空 call stack 已經完整走過一遍,而這一遍其實順手示範了三個平常被當口訣背的名詞。下一段回頭替其中的 hoisting 補上完整規則。

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 初始化,所以可以在宣告之前呼叫。

temporal dead zone 的區間圖,三種宣告在時間軸上的差異只有這張圖說得清楚。
9:36 · temporal dead zone 的區間圖,三種宣告在時間軸上的差異只有這張圖說得清楚。
承上 承上一段結尾的觀察:剛剛那一遍走查已經順手示範了三個名詞,作者先把其中的 hoisting 講完整。

推理因為上一段完成了一次從頭到尾的執行,所以作者接著回頭把 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 初始化,所以可以在宣告之前呼叫。

AI 補充三種待遇的差別其實只有一個變數:creation phase 結束時那個 binding 手上拿到什麼——什麼都沒有(封條狀態)、undefined、還是一個完整的 function object。順著這個框架,兩個常見疑問也一起解決了。第一,temporal dead zone 的起點是所在區塊的開頭而不是檔案開頭,因為每進入一個區塊就換一個新的 environment record,死區自然也以區塊為單位。第二,在同一個區塊裡用 let 重複宣告同一個名字得到的是 SyntaxError 而不是 ReferenceError,那發生在更早的解析階段,和 hoisting 無關。另外值得補一句:var 的 undefined 之所以危險,正是因為它讓錯誤靜默——提前使用不會爆炸,只是拿到一個看似合理的值,錯誤要到很後面才浮現;const 與 let 的死區是刻意選擇讓它早點爆炸。

temporal dead zone

暫時性死區(TDZ)

從區塊開始到 const、let 或 class 宣告那一行為止的區間,在其中存取該 binding 會得到 ReferenceError。

名字裡的 temporal 強調它是時間上的區間而不是程式碼位置:同一行程式可能在一次執行中位於死區、在另一次不是。它的設計目的是把「在賦值前使用」這種錯誤變成立即失敗,而不是像 var 那樣悄悄拿到 undefined。函式參數的預設值運算式也有自己的死區規則,這是它最容易讓人踩到的角落。

相關術語: uninitialized (由其造成)、ReferenceError (觸發)

出處:第 11 段「Hoisting 的三種待遇與 TDZ」

ReferenceError

參照錯誤

存取尚未初始化、或在整條作用域鏈上都不存在的 binding 時丟出的錯誤。

它有兩個來源長得很像但成因不同:名字根本沒有被宣告(沿 scope chain 一路找到 null 都沒命中),以及名字已經登記但仍處於未初始化狀態(死區)。錯誤訊息通常會區分「is not defined」與「Cannot access before initialization」,讀訊息就能判斷是哪一種。它和 TypeError 的差別在於:找不到名字是 ReferenceError,找到了但用錯方式才是 TypeError。

相關術語: temporal dead zone (常見來源)、scope chain (查找失敗於)

出處:第 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
留給下一段 hoisting 已經結案,但剛剛那條沿 outer env 往外找的路徑、以及「函式記得住外層變數」這件事,都還沒被收成一句定義。下一段用同一組零件把它們收尾。

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。

closure 成立的關鍵圖:inner 的 function environment record 的 outer env 指回外層函式的 environment record。
11:06 · closure 成立的關鍵圖:inner 的 function environment record 的 outer env 指回外層函式的 environment record。
承上 承上一段留下的收尾工作:把「沿 outer env 往外找」這條路徑、以及「函式記得住外層變數」這件事,各自收成一句定義。

推理因為上一段已經用 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。

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 三個名詞,只是這同一疊結構在不同時間點呈現出來的三個側面。

closure

閉包

內層函式透過 function object 的 environment 屬性保留外層 environment record 參照,因而形成的結構。

常見誤解是以為閉包把變數「複製」了一份,實際上它保留的是對整個 environment record 的參照,所以外層變數之後被修改,閉包看到的也是新值。另一個推論是記憶體:只要閉包還活著,它所參照的整個 environment record 就無法被回收,這是閉包造成記憶體洩漏的原理。實務上模組模式、私有狀態、事件處理器與各種工廠函式,本質上都是同一件事的不同用法。

相關術語: function object (靠其 environment)、scope chain (沿其查找)

出處:第 12 段「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

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