一、執行上下文(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>

當進入一個執行上下文(比如函數調用或全局腳本)時,通常有兩步:

  1. ​**解析/創建階段(Creation)**​:處理聲明
    1. 所有函數聲明會被綁定(並指向函數對象);
    2. var 聲明會在變量環境中創建並初始化為 undefined (這就是“提升”表現);
    3. let/const 則會在詞法環境中記錄但處於 TDZ(Temporal Dead Zone)直到初始化。
  2. ​**執行階段(Execution)**​:執行代碼,賦值、運行語句、創建閉包等。

1 Environment Record 的三種實現形式

環境記錄是 JS 變量體系的“底層數據結構”。

1.1 DeclarativeEnvironmentRecord(DER)

​綁定絕大多數作用域,不會掛載到全局,也不會被 delete,​用來存:

  • let
  • const
  • class
  • 函數的形參(包括默認值表達式)
  • 內部函數聲明(非全局的 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 處理器,會報未處理拒絕。