[2-1] 执行上下文与闭包 · 执行机制与上下文(Execution Context & Call Stack)

10 阅读4分钟

所属板块:2. 执行上下文与闭包(JS 的核心引擎)

记录日期:2026-03-xx
更新:遇到执行上下文或调用栈相关输出题时补充

1. JS 是单线程的同步执行语言

JS 主线程一次只能做一件事,所有代码都在主调用栈上按顺序执行。
这也是为什么异步(setTimeout、Promise)需要事件循环来配合——但那是后面板块的内容,这里先记下:JS 本身没有多线程,所有上下文都在同一个调用栈里调度。

2. 执行上下文(Execution Context):代码运行的“容器”

执行上下文就是 JS 引擎为一段代码创建的运行环境。它包含当前代码能访问的所有信息。

主要分类:

  • 全局执行上下文(Global EC):程序启动时自动创建,只有一个,this 指向全局对象(浏览器 window / Node global)
  • 函数执行上下文(Function EC):每次函数调用时创建一个新的
  • eval 执行上下文(极少使用,可忽略)

3. 调用栈(Call Stack):上下文的调度机制

JS 用 栈结构(LIFO 后进先出) 管理所有执行上下文:

  • 全局上下文永远在栈底
  • 函数调用时,新上下文压入栈顶
  • 函数执行完毕,出栈并销毁

栈溢出(Stack Overflow)本质:调用栈空间耗尽(无限递归或调用链过深)。

简单示例 + 栈变化:

function outer() {
  inner();
}

function inner() {
  console.log("inner");
}

outer();

栈过程:

  1. 全局 EC 入栈(栈底)
  2. outer() 调用 → outer EC 入栈
  3. inner() 调用 → inner EC 入栈
  4. inner 执行完 → inner 出栈
  5. outer 执行完 → outer 出栈
  6. 全局结束 → 全局出栈

4. 执行上下文的生命周期(创建阶段 vs 执行阶段)

每个上下文都严格经历两个阶段:

创建阶段(引擎在“编译”时就做的准备工作)

  1. 创建变量对象(Variable Object / VO)

    • ES3 视角:也叫活动对象(Activation Object / AO)
    • 收集 var 声明 → 初始化 undefined
    • 收集 function 声明 → 完整函数体
    • 收集形参
  2. 创建作用域链(Scope Chain)

    • 作用域链本质是一系列外部环境引用的链表(每个上下文都持有指向外层上下文的 [[outer]] 指针)。
    • 它的作用是在后续查找变量时,提供“从内向外顺藤摸瓜”的路径(词法作用域的物理载体)。
    • (详细规则将在【2-2】展开)
  3. 确定 this 指向

    • this 是执行上下文中的一个特殊属性,它的绑定规则在函数被调用时才最终确定(默认绑定、隐式绑定、显式绑定、new 绑定、箭头函数词法 this)。
    • 一旦确定,在整个执行阶段都不会再改变。
    • (详细五大规则将在【2-3】展开)

ES6 后更准确的说法是:

  • 词法环境(Lexical Environment):负责 let/const、块级作用域、闭包
  • 变量环境(Variable Environment):负责 var、函数声明(兼容老规范)

执行阶段

逐行运行代码,进行赋值、函数调用等操作。

5. 变量提升(Hoisting)——创建阶段的核心表现

提升的是声明,不是赋值:

  • var:提升到当前上下文顶部,值为 undefined
  • function 声明:完整提升(函数名 + 函数体),优先级高于 var
  • let / const:有提升,但进入暂时性死区(TDZ),提前访问报 ReferenceError

示例对比:

console.log(a);     // undefined
var a = 100;

console.log(b);     // ReferenceError(TDZ)
let b = 200;

foo();              // "函数提升"
function foo() {
  console.log("函数提升");
}

同名优先级:函数声明 > var 声明(函数先占位,后被 var 赋值覆盖)。

6. 暂时性死区(TDZ)的设计哲学

let / const 被故意锁在 TDZ 中,是为了避免 var 那种“声明前可用却 undefined”的隐蔽 bug,同时实现块级作用域。

经典 TDZ 题:

let x = 1;
function test() {
  console.log(x);   // ReferenceError(TDZ,函数内 let x 遮蔽了外部 x)
  let x = 2;
}
test();

7. 小结 & 复习时的“引擎视角”

遇到任何“变量 undefined / ReferenceError / 函数提前可用”的题,第一反应都是:

  1. 当前在哪个执行上下文?
  2. 调用栈当前栈顶是谁?
  3. 变量是在创建阶段的 VO / Lexical Environment 里如何初始化的?
  4. 有没有触发 TDZ 或提升优先级问题?

掌握了执行上下文和调用栈,就等于拿到了 JS 引擎的“运行日志”,后面作用域链、this、闭包都会变得顺理成章。

下一篇文章会进入:作用域与作用域链(词法作用域、LHS/RHS 查询等)。

返回总目录:戳这里