一、執行上下文(Execution Context)與環境記錄(Environment Record)
執行上下文是函數/全局/模塊代碼執行時的抽象環境,包含幾部分:
- LexicalEnvironment(詞法環境)——存 let/const/class、catch 參數、塊級作用域、函數參數、箭頭函數 this 等詞法綁定
- 不掛載到 global object
- 有 TDZ(Temporal Dead Zone)
- VariableEnvironment(變量環境)——用於 var 與 function 函數聲明的綁定。
- 變量提升到
undefined - 可寫可刪性有限(var 聲明的全局屬性不可被 delete)
- 變量提升到
- ThisBinding(this 綁定)等。
舊書裏常講 VO 是全局的變量對象,AO 是函數的變量對象,而 VO 會綁定到 GO(window/global)上。這些等等都是 ES5/更早表述,我們這裏就不再談論了。
ER(環境記錄)是用來真正“存變量、查變量”的地方。
Execution Context:
LexicalEnvironment: {
EnvironmentRecord (DeclarativeEnvironmentRecord)
outer: parent LexicalEnvironment
}
VariableEnvironment: {
EnvironmentRecord (DeclarativeEnvironmentRecord)
outer: parent LexicalEnvironment
}
ThisBinding: <value>
當進入一個執行上下文(比如函數調用或全局腳本)時,通常有兩步:
- **解析/創建階段(Creation)**:處理聲明
- 所有函數聲明會被綁定(並指向函數對象);
var聲明會在變量環境中創建並初始化為undefined(這就是“提升”表現);let/const則會在詞法環境中記錄但處於 TDZ(Temporal Dead Zone)直到初始化。
- **執行階段(Execution)**:執行代碼,賦值、運行語句、創建閉包等。
1 Environment Record 的三種實現形式
環境記錄是 JS 變量體系的“底層數據結構”。
1.1 DeclarativeEnvironmentRecord(DER)
綁定絕大多數作用域,不會掛載到全局,也不會被 delete,用來存:
letconstclass- 函數的形參(包括默認值表達式)
- 內部函數聲明(非全局的 function)
catch(e)的e- 箭頭函數沒有 ThisBinding(this),但內部引用的變量也是 DER 查找
每個 DER 長這樣:
DeclarativeEnvironmentRecord:
bindings: {
變量名: {
value: <值>,
mutable: true/false,
initialized: true/false, // 用於 TDZ
deletable: true/false,
strict: true/false
}
}
TDZ 其實是更滿足我們直觀的編程特性,很多其他語言天然支持,so JavaScript 又在彌補自己的設計缺陷?
console.log(a); // ReferenceError
let a = 1;
1.2 ObjectEnvironmentRecord(OER)
綁定到一個實際對象,用於 “使用對象作為作用域” 的場景。
注意:這裏的使用對象作為作用域和塊級作用域寫法都是
{},一個是作用域塊,一個是對象,但卻是兩個不同的東西。
1.2.1 全局作用域中的 var/function 聲明
全局執行上下文的 VE 用 OER 來容納 var 和 function 聲明,其底層對象就是全局對象,OER 直接操作 globalObject 的屬性:
var a = 1;
console.log(window.a); // 1
1.2.2 with 語句作用域
with(obj) 會創建一個 OER,將 obj 當成作用域鏈的一部分,使得“找變量時”會先查這個對象屬性。
const o = { x: 1 };
with(o) {
console.log(x); // 來自 o.x
}
with 語句極少使用。
ObjectEnvironmentRecord:
object: <綁定的對象>
bindings: 就是 object 的屬性
OER 內沒有“自有存儲”,所有變量查找實際上是 屬性查找。
1.3 FunctionEnvironmentRecord(FER)
專用於函數執行上下文,它是函數專屬的一種 ER,包含函數獨有的數據:
- 函數的內部綁定(var/function 聲明)
- 參數(包括默認值、rest 參數)
- thisBinding
- superBinding
- homeObject(用於 super)
- new.target
FunctionEnvironmentRecord:
thisBinding: <值>
thisBindingStatus: lexical / initialized / uninitialized
functionObject: fn 本身
homeObject: 用於 super 的對象
newTarget: new.target 的值
bindings:
var/function 聲明
參數名稱 → 綁定
函數內 let/const 會進入另一個 DeclarativeEnvironmentRecord(LE),而不是進入 FER。
一個函數執行時:
Execution Context
├─ LexicalEnvironment
│ ├─ DeclarativeEnvironmentRecord(let/const)
│ └─ outer → 上層作用域
├─ VariableEnvironment
│ └─ FunctionEnvironmentRecord(var/參數)
└─ ThisBinding 由調用決定
全局執行時:
LexicalEnvironment:
DER(let/const)
VariableEnvironment:
OER(var/function → global object)
ThisBinding: global object(非嚴格)
二、變量提升(Hoisting)與 TDZ(Temporal Dead Zone)
function聲明:整體提升(函數體也提升)—— 在創建階段函數對象已綁定,可以直接調用(在同一執行上下文)。var:變量名提升到變量環境,初始值undefined。let/const:不會被初始化為undefined,在創建階段記錄但進入 TDZ,在實際執行到聲明處才完成初始化。訪問 TDZ 會拋ReferenceError。class:類似於let(處於 TDZ,類聲明不會提升到可用狀態)。
三、作用域鏈與閉包(Scope Chain & Closure)
- 作用域鏈:每個執行上下文的詞法環境都有一個
outer指針,形成鏈式結構。查找變量時,從當前環境往外查直到全局。 - 閉包:當一個內部函數引用了外部函數的變量,外部函數的詞法環境就不會被 GC(只要閉包可達),從而形成閉包。閉包保存的是對環境記錄(變量的引用),不是對變量值的“拷貝”。
function makeCounter() {
let count = 0;
return function () {
return ++count;
};
}
const c = makeCounter();
c(); // 1
三、報錯時的執行上下文與 Stack Trace(“報錯執行上下文”)
- 拋出錯誤(
throw)會立刻打斷當前執行上下文的正常流,開始查找最近的try...catch:- 若找到匹配的
catch,控制權跳轉到catch塊(catch會創建新的詞法環境記錄exception綁定); - 若沒有,錯誤會沿調用棧向上拋,最終變為未捕獲異常(瀏覽器在控制枱顯示 stack trace,Node 會導致進程退出或觸發
unhandledRejection/uncaughtException)。
- 若找到匹配的
- Promise 內拋錯誤:若未處理會進入 Promise 的 rejected 狀態;若沒有
.catch()或全局unhandledrejection處理器,會報未處理拒絕。