js-执行上下文 一、执行上下文的本质定义执行上下文Execution Context, EC是 ECMAScript 规范中定义的抽象概念代表一段可执行代码被评估和执行时的完整环境状态。从实现角度看它是引擎在内存中维护的一个结构化数据对象包含执行所需的所有绑定信息。二、三种执行上下文的精确分类类型创建时机数量销毁时机全局执行上下文GEC脚本启动时且仅有1个页面关闭/进程终止函数执行上下文FEC每次函数调用时无限制按调用次数函数返回除闭包外Eval 执行上下文EECeval()调用时取决于调用次数代码执行完毕关键点函数定义时不创建上下文只有调用时才创建。三、执行上下文的核心组件规范视角根据 ECMAScript 规范每个执行上下文都包含以下必需组件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// 示例代码 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 - 注意不覆盖已存在的同名函数/参数 步骤4let/const 处理 - 扫描 let/const 声明 - 创建绑定但标记为未初始化 - 进入暂时性死区此时变量环境的结构VariableEnvironment { param: 10, b: function, // 函数声明 a: undefined, // var 声明 // let/const 不在此处 }4.2 创建词法环境处理 let/constLexicalEnvironment { c: uninitialized, // 未初始化不可访问 d: uninitialized }4.3 确定 this 绑定ThisBinding 的确定规则按优先级从高到低调用方式this 指向new fn()新创建的对象fn.call(obj)/fn.apply(obj)指定的 objobj.fn()obj普通函数调用非严格模式全局对象浏览器 window普通函数调用严格模式undefined箭头函数继承外层 this事件处理器绑定的 DOM 元素第二阶段执行阶段Execution Phase代码开始逐行执行引擎实时修改环境记录中的绑定值。// 执行阶段逐行推进 var a 1; // a 从 undefined → 1 let c 2; // c 从未初始化 → 2此时离开暂时性死区 const d 3; // d 从未初始化 → 3不可再赋值第三阶段销毁阶段一般情况下函数返回后执行上下文被标记为可回收下次 GC 时回收内存。特殊例外闭包如果内部函数持有外部函数变量的引用外部函数的环境记录不会被销毁。function outer() { var secret still here; return function inner() { console.log(secret); // 持有 secret 的引用 }; } const fn outer(); // outer 的上下文不会销毁因为 secret 被 inner 引用五、执行上下文栈ECS调用管理的核心机制JS 引擎使用栈数据结构管理执行上下文确保正确的执行顺序。5.1 栈的底层行为function a() { b(); } function b() { c(); } function c() { throw new Error(trace); } a();抛出错误的堆栈信息从底向上读at c (repl:3:18) at b (repl:2:13) at a (repl:1:11) at repl:1:1对应的栈状态变化压栈视角初始[全局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,000SpiderMonkey (Firefox)~50,000JavaScriptCore (Safari)~10,000超过限制触发RangeError: Maximum call stack size exceeded六、作用域链的底层原理很多人理解作用域链是链条但规范中的实现更精确。6.1 词法环境嵌套结构每个环境记录有一个[[OuterEnv]]引用指向外层环境。// 嵌套结构示意 GlobalEnvironment { records: { ... }, [[OuterEnv]]: null // 全局的外层是 null } FunctionEnvironment { records: { ... }, [[OuterEnv]]: GlobalEnvironment // 指向外层 } InnerFunctionEnvironment { records: { ... }, [[OuterEnv]]: FunctionEnvironment // 指向外层 }6.2 标识符解析算法当引擎遇到变量x时执行以下查找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 时代当前标准用LexicalEnvironment和VariableEnvironment替代 VO/AO更精确地区分let/const→ 词法环境var/function→ 变量环境核心差异变量环境在创建阶段完全初始化词法环境的let/const保持未初始化直到执行阶段。八、完整代码的底层执行模拟用这个示例把所有概念串起来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);分步执行记录━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 步骤 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 ThisBindingglobalThis 【执行阶段】 outerVar O outerLet OL inner 函数定义完成inner.[[Scope]] outer 的环境 调用 inner(40) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 步骤 2调用 inner(40) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 【创建阶段】 参数绑定z 40 变量环境innerVar: undefined [[OuterEnv]]指向 outer 的环境 【执行阶段】 innerVar I 查找 globalVarinner 没有 → outer 没有 → GlobalEnv 有 → G 查找 yinner 没有 → outer 有 → 20 查找 outerVarinner 没有 → outer 有 → O 查找 zinner 有 → 40 查找 innerVarinner 有 → I 返回拼接结果G 20 O 40 I ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 步骤 3-5逐层返回并销毁 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ inner 返回 → innerEC 弹出销毁 outer 返回 → outerEC 弹出销毁无闭包引用 全局执行上下文持续存在九、常见误区澄清误区真相函数定义时就创建执行上下文函数调用时才创建上下文包含所有代码只有可执行代码全局、函数、eval会触发创建闭包保持整个上下文存活只保持被引用的环境记录不是完整上下文this在创建阶段就确定对且不会在执行阶段改变箭头函数除外变量提升是引擎移动了代码不移动只是创建阶段提前声明十、一句话总结执行上下文是 JS 引擎在运行全局代码或调用函数时创建的规范对象包含词法环境let/const、变量环境var/function和 this 绑定经历创建→执行→销毁三阶段通过栈结构管理调用顺序。把这段话拆解记住什么时候创建脚本启动、函数调用时包含什么环境记录 this 绑定经历什么创建声明绑定→ 执行赋值运行→ 销毁回收如何管理调用栈压栈出栈