js-执行上下文

一、执行上下文的本质定义

执行上下文(Execution Context, EC) 是 ECMAScript 规范中定义的抽象概念 ,代表一段可执行代码被评估和执行时的完整环境状态

从实现角度看,它是引擎在内存中维护的一个结构化数据对象,包含执行所需的所有绑定信息。


二、三种执行上下文的精确分类

类型 创建时机 数量 销毁时机
全局执行上下文(GEC) 脚本启动时 且仅有1个 页面关闭/进程终止
函数执行上下文(FEC) 每次函数调用时 无限制(按调用次数) 函数返回(除闭包外)
Eval 执行上下文(EEC) eval() 调用时 取决于调用次数 代码执行完毕

关键点:函数定义时不创建上下文,只有调用时才创建


三、执行上下文的核心组件(规范视角)

根据 ECMAScript 规范,每个执行上下文都包含以下必需组件

TypeScript 复制代码
interface ExecutionContext {
  // 1. 词法环境:处理 let/const 声明
  LexicalEnvironment: EnvironmentRecord;
  
  // 2. 变量环境:处理 var/function 声明
  VariableEnvironment: EnvironmentRecord;
  
  // 3. this 绑定:当前上下文中的 this 值
  ThisBinding: any;
  
  // 4. 额外的追踪信息(引擎实现相关)
  // 例如:代码执行位置、回收标记等
}

3.1 环境记录(Environment Record)是什么?

环境记录是一个规范类型 ,本质上是一个键值存储结构,负责维护标识符绑定。

分为两种:

声明式环境记录

  • 存储变量、函数、参数

  • 用于函数、模块、catch 块

对象式环境记录

  • 包装外部对象(如 window

  • 用于全局作用域


四、生命周期:创建 → 执行 → 销毁

这是理解执行上下文最核心的部分。每个上下文都严格经历三个阶段

第一阶段:创建阶段(Creation Phase)

此时代码一行都没执行,引擎完成三件事:

4.1 创建变量环境(处理 var/function)
javascript 复制代码
// 示例代码
function test(param) {
  var a = 1;
  function b() {}
  let c = 2;
  const d = 3;
}
test(10);

创建阶段发生的事情(严格按顺序):

复制代码
步骤1:函数参数初始化
  - 创建参数绑定:param = 10

步骤2:函数声明提升
  - 扫描函数声明 b
  - 完整函数体存入环境
  - 注意:如果已有同名参数,函数声明会覆盖

步骤3:变量声明提升(var)
  - 扫描 var 声明 a
  - 创建绑定 a = undefined
  - 注意:不覆盖已存在的同名函数/参数

步骤4:let/const 处理
  - 扫描 let/const 声明
  - 创建绑定但标记为"未初始化"
  - 进入"暂时性死区"

此时变量环境的结构:

javascript 复制代码
VariableEnvironment = {
  param: 10,
  b: function,      // 函数声明
  a: undefined,     // var 声明
  // let/const 不在此处
}
4.2 创建词法环境(处理 let/const)
javascript 复制代码
LexicalEnvironment = {
  c: <uninitialized>,  // 未初始化,不可访问
  d: <uninitialized>
}
4.3 确定 this 绑定

ThisBinding 的确定规则(按优先级从高到低):

调用方式 this 指向
new fn() 新创建的对象
fn.call(obj) / fn.apply(obj) 指定的 obj
obj.fn() obj
普通函数调用(非严格模式) 全局对象(浏览器 window)
普通函数调用(严格模式) undefined
箭头函数 继承外层 this
事件处理器 绑定的 DOM 元素

第二阶段:执行阶段(Execution Phase)

代码开始逐行执行,引擎实时修改环境记录中的绑定值。

javascript 复制代码
// 执行阶段逐行推进
var a = 1;     // a 从 undefined → 1
let c = 2;     // c 从未初始化 → 2(此时离开暂时性死区)
const d = 3;   // d 从未初始化 → 3,不可再赋值

第三阶段:销毁阶段

一般情况下:函数返回后,执行上下文被标记为可回收,下次 GC 时回收内存。

特殊例外(闭包):如果内部函数持有外部函数变量的引用,外部函数的环境记录不会被销毁。

javascript 复制代码
function outer() {
  var secret = 'still here';
  return function inner() {
    console.log(secret);  // 持有 secret 的引用
  };
}
const fn = outer();  
// outer 的上下文不会销毁,因为 secret 被 inner 引用

五、执行上下文栈(ECS):调用管理的核心机制

JS 引擎使用栈数据结构管理执行上下文,确保正确的执行顺序。

5.1 栈的底层行为

javascript 复制代码
function a() { b(); }
function b() { c(); }
function c() { throw new Error('trace'); }
a();

抛出错误的堆栈信息(从底向上读):

bash 复制代码
at c (repl:3:18)
at b (repl:2:13)
at a (repl:1:11)
at repl:1:1

对应的栈状态变化(压栈视角):

bash 复制代码
初始:[全局EC]

调用 a():  [全局EC, aEC]
a 内调用 b():[全局EC, aEC, bEC]
b 内调用 c():[全局EC, aEC, bEC, cEC]
c 执行完毕:[全局EC, aEC, bEC]    ← cEC 弹出并销毁
b 执行完毕:[全局EC, aEC]         ← bEC 弹出并销毁
a 执行完毕:[全局EC]              ← aEC 弹出并销毁

5.2 栈的最大深度限制

不同引擎的调用栈限制(约数):

引擎 最大调用深度
V8 (Chrome/Node) ~10,000 - 20,000
SpiderMonkey (Firefox) ~50,000+
JavaScriptCore (Safari) ~10,000

超过限制触发 RangeError: Maximum call stack size exceeded


六、作用域链的底层原理

很多人理解作用域链是"链条",但规范中的实现更精确。

6.1 词法环境嵌套结构

每个环境记录有一个 [[OuterEnv]] 引用,指向外层环境。

javascript 复制代码
// 嵌套结构示意
GlobalEnvironment = {
  records: { ... },
  [[OuterEnv]]: null        // 全局的外层是 null
}

FunctionEnvironment = {
  records: { ... },
  [[OuterEnv]]: GlobalEnvironment  // 指向外层
}

InnerFunctionEnvironment = {
  records: { ... },
  [[OuterEnv]]: FunctionEnvironment  // 指向外层
}

6.2 标识符解析算法

当引擎遇到变量 x 时,执行以下查找:

bash 复制代码
1. 在当前环境记录中查找 'x'
2. 如果找到 → 返回绑定的值
3. 如果没找到 → 沿 [[OuterEnv]] 进入外层环境
4. 重复步骤 1-3
5. 直到 [[OuterEnv]] === null
6. 如果全局也找不到 → 抛出 ReferenceError

时间复杂度:理论上 O(作用域链深度),通常很小(< 10 层)。


七、VO/AO 的历史演变(ES3 → ES5+)

如果你看过老文章,会遇到 VO 和 AO 这两个术语。

ES3 时代

  • 变量对象(VO):全局上下文中使用的环境

  • 激活对象(AO):函数上下文中的变量对象,额外存储参数

  • 两者本质相同,只是创建时机不同

ES5+ 时代(当前标准)

LexicalEnvironmentVariableEnvironment 替代 VO/AO,更精确地区分:

  • let/const → 词法环境

  • var/function → 变量环境

核心差异 :变量环境在创建阶段完全初始化,词法环境的 let/const 保持未初始化直到执行阶段。


八、完整代码的底层执行模拟

用这个示例把所有概念串起来:

bash 复制代码
var globalVar = 'G';
let globalLet = 'GL';

function outer(y) {
  var outerVar = 'O';
  let outerLet = 'OL';
  
  function inner(z) {
    var innerVar = 'I';
    return globalVar + y + outerVar + z + innerVar;
  }
  
  return inner(40);
}

outer(20);

分步执行记录

bash 复制代码
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤 0:创建全局执行上下文(创建阶段)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
变量环境:
  globalVar: undefined
  outer: function  ← 完整函数体

词法环境:
  globalLet: <uninitialized>

ThisBinding: globalThis

作用域链:
  GlobalEnv.[[OuterEnv]] = null

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤 0-执行:运行全局代码
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
globalVar = 'G'        // 变量环境更新
globalLet = 'GL'       // 词法环境更新(离开 TDZ)
outer 函数定义完成     // outer.[[Scope]] = GlobalEnv

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤 1:调用 outer(20)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【创建阶段】
  参数绑定:y = 20
  变量环境:outerVar: undefined, inner: function
  词法环境:outerLet: <uninitialized>
  [[OuterEnv]]:指向 GlobalEnv
  ThisBinding:globalThis

【执行阶段】
  outerVar = 'O'
  outerLet = 'OL'
  inner 函数定义完成(inner.[[Scope]] = outer 的环境)
  
  调用 inner(40):

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤 2:调用 inner(40)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【创建阶段】
  参数绑定:z = 40
  变量环境:innerVar: undefined
  [[OuterEnv]]:指向 outer 的环境

【执行阶段】
  innerVar = 'I'
  
  查找 globalVar:inner 没有 → outer 没有 → GlobalEnv 有 → 'G'
  查找 y:inner 没有 → outer 有 → 20
  查找 outerVar:inner 没有 → outer 有 → 'O'
  查找 z:inner 有 → 40
  查找 innerVar:inner 有 → 'I'
  
  返回拼接结果:'G' + 20 + 'O' + 40 + 'I'

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤 3-5:逐层返回并销毁
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
inner 返回 → innerEC 弹出销毁
outer 返回 → outerEC 弹出销毁(无闭包引用)
全局执行上下文持续存在

九、常见误区澄清

误区 真相
函数定义时就创建执行上下文 函数调用时才创建
上下文包含所有代码 只有可执行代码(全局、函数、eval)会触发创建
闭包保持整个上下文存活 只保持被引用的环境记录,不是完整上下文
this 在创建阶段就确定 对,且不会在执行阶段改变(箭头函数除外)
变量提升是引擎"移动"了代码 不移动,只是创建阶段提前声明

十、一句话总结

执行上下文是 JS 引擎在运行全局代码或调用函数时创建的规范对象,包含词法环境(let/const)、变量环境(var/function)和 this 绑定,经历创建→执行→销毁三阶段,通过栈结构管理调用顺序。

把这段话拆解记住:

  • 什么时候创建:脚本启动、函数调用时

  • 包含什么:环境记录 + this 绑定

  • 经历什么:创建(声明绑定)→ 执行(赋值运行)→ 销毁(回收)

  • 如何管理:调用栈(压栈出栈)

相关推荐
咪饭只吃一小碗11 小时前
JS 打工记:同步搬砖、异步摸鱼,Promise 来救场
前端·javascript·面试
用户7138742290011 小时前
彻底搞懂浏览器客户端存储:从 localStorage 到完整存储体系
前端
bonechips12 小时前
告别 var,拥抱 let 和 const:JavaScript 变量声明完全指南
javascript·代码规范
如果超人不会飞12 小时前
别再自己套壳了!三分钟把你的浏览器变成 AI 的“提线木偶”——WebMCP 深度解析
javascript
Land032912 小时前
RPA替代方案:离线部署与Python扩展实战
开发语言·python·rpa
郝学胜-神的一滴12 小时前
Qt 高级开发 017:中文乱码
开发语言·c++·qt·程序人生·用户界面
掘金安东尼12 小时前
前端周刊第 467 期 🗂 本期精选目录
前端
qcx2312 小时前
【系统学AI】07 ReAct范式:从奠基之作到Reflexion/RAF的演进
前端·人工智能·react.js
无糖可可果12 小时前
从 Python List 到 LLM:一个开发者的 AI 学习之路
前端