每当一段代码要运行时,JavaScript引擎(或其他语言的运行时环境)就会为其创建一个“执行上下文”,用来管理和跟踪该段代码的执行状态。
每个执行上下文在创建时通常包含以下几个核心组件:
1.变量环境(存储var):
- 存储在该上下文中通过var声明的变量和函数声明。
- 在上下文创建阶段(编译阶段)就会被初始化(变量提升)。
- let和const声明的变量存在于词法环境中。
2.词法环境(存储let和const):
- ES6引入的概念,用于更精确地管理作用域。
- 存储在该上下文中通过let和const声明的变量和函数声明。
- 也存储着对外部词法环境的引用,形成了作用域链的基础。
- 在代码执行阶段才会被初始化(存在“暂时性死区”)。
3.作用域链:
- 由当前词法环境及其所有外部(父级)词法环境的引用串联而成的一条链。
- 决定了当前代码可以访问哪些变量和函数。当在当前上下文中查找一个标识符时,引擎会沿着作用域链逐级向外查找,直到找到该标识符或到达全局作用域(找不到则报错)。
- 作用域链在执行上下文创建时就被确定(词法作用域/静态作用域),由代码的书写位置决定,与调用位置无关。
4.this绑定:
- 指向当前执行上下文所属的“所有者”对象。
- 其值在函数被调用时确定(运行时绑定),取决于函数的调用方式:
- 普通函数调用:this指向全局对象(浏览器中是window,严格模式下是undefined)。
- 方法调用:this指向调用该方法的对象。
- new调用(构造函数):this指向新创建的对象实例。
- call()/apply()/bind():this指向传入的第一个参数指定的对象。
- 箭头函数:没有自己的this,继承自其定义时所在执行上下文的this值(词法作用域)。
01执行上下文的生命周期
1.创建阶段:
创建变量对象(VO)/ 初始化变量环境(VE)和词法环境(LE)。
- 扫描函数声明:创建函数标识符并指向函数体。
- 扫描var变量声明:创建标识符,初始化为undefined。
- 扫描let/const变量声明:创建标识符,但不初始化(处于“暂时性死区”)。
建立作用域链:确定当前上下文的外部引用。
确定this的值(对于函数执行上下文)。
2.执行阶段:
- 按顺序逐行执行代码。
- 执行变量赋值。
- 执行函数调用(遇到函数调用会创建新的执行上下文)。
- 遇到let/const声明时进行初始化赋值(此时才脱离“暂时性死区”)。
3.销毁阶段:
- 当代码执行完毕,该执行上下文通常会被销毁(垃圾回收)。
- 例外:闭包。如果一个内部函数引用了外部函数执行上下文中的变量,那么即使外部函数执行完毕,其执行上下文也不会被立即销毁(因为内部函数的作用域链还保持着对它的引用),直到所有引用了它的内部函数都不再可用。
02执行上下文的类型
1.全局执行上下文:
- 代码开始执行时首先创建。
- 只有一个。
- 创建全局对象(浏览器中是window,Node.js中是global),并将this指向它。
- 将全局变量和函数声明添加到全局上下文中。
- 在程序退出(如关闭浏览器标签页)时销毁。
2.函数执行上下文:
- 每次调用函数时都会创建一个新的函数执行上下文。
- 包含该函数内部的变量、参数、作用域链(指向定义该函数时的作用域链+自身活动对象)、this。
- 函数执行完毕后,其执行上下文通常会被销毁(除非形成闭包)。
3.Eval执行上下文:
- 在eval()函数内部执行的代码拥有自己的执行上下文。
- 由于其安全性和性能问题,现代开发中很少使用eval()。
03执行上下文栈(调用栈)
JavaScript引擎使用一个栈数据结构(LIFO - 后进先出)来管理所有的执行上下文,称为执行栈或调用栈。

流程:
- 程序启动,创建全局执行上下文(Global EC),将其压入栈底。
- 当调用一个函数时,为该函数创建一个新的函数执行上下文(Function EC),并将其压入执行栈的顶部。
- 函数开始执行。在函数执行过程中,它可能又调用了其他函数,为每个被调用的函数创建新的执行上下文并压栈。
- 当函数执行完毕,它的执行上下文会从栈顶弹出(销毁或标记为可回收)。
- 控制权交还给栈中下一个(下方)的执行上下文。
- 当所有代码执行完毕,全局执行上下文最终也会被弹出栈(程序结束)。
1function outer() {
2 console.log('Outer start');
3 inner(); // 步骤3:调用inner,创建inner EC并压栈
4 console.log('Outer end');
5}
6
7function inner() {
8 console.log('Inner'); // 步骤4:inner执行完毕,弹出inner EC
9}
10
11console.log('Global start'); // 步骤1:全局EC压栈
12outer(); // 步骤2:调用outer,创建outer EC并压栈
13console.log('Global end'); // 步骤6:所有函数执行完毕,最后弹出全局EC
调用栈状态变化:
- Global EC入栈 -> 执行console.log('Global start') -> 调用outer()
- outer EC入栈 -> 执行console.log('Outer start') -> 调用inner()
- inner EC入栈 -> 执行console.log('Inner') -> inner EC执行完毕,出栈
- 控制权回到outer EC -> 执行console.log('Outer end') -> outer EC执行完毕,出栈
- 控制权回到Global EC -> 执行console.log('Global end') -> Global EC执行完毕,出栈(程序结束)