在执行 JavaScript 代码时,可能会存在多个 执行上下文(Execution Context),JavaScript 引擎通过 执行上下文栈(Execution Context Stack,ECS) 来管理这些上下文。执行上下文栈是一种 后进先出(LIFO,Last In First Out) 的数据结构,它用于存储和管理 JavaScript 代码执行过程中的上下文信息。


1. JavaScript 引擎如何管理执行上下文

JavaScript 引擎的执行流程主要涉及 创建执行上下文、入栈、执行代码、出栈 这几个步骤。以下是详细的管理机制:

(1) 执行上下文栈(ECS)的结构

执行上下文栈类似一个 调用栈,它管理代码的执行顺序。栈中的第一个上下文是 全局执行上下文,后续的函数调用会创建新的执行上下文并压入栈中,而执行完毕后会弹出栈。

栈的管理过程

  1. 初始化阶段

    • JavaScript 代码开始执行时,会创建 全局执行上下文,并将其压入执行上下文栈(ECS)。
    • this 绑定到 全局对象windowglobal)。
  2. 函数调用阶段

    • 每当 JavaScript 执行一个函数时,都会创建一个新的 函数执行上下文,并将其压入执行上下文栈的顶部。
  3. 执行完毕出栈

    • 当函数执行完毕后,其对应的执行上下文会从执行上下文栈中弹出,控制权交回上一个执行上下文。
  4. 整个程序执行完毕

    • 当所有的函数调用都完成后,全局执行上下文最终也会从执行上下文栈中弹出,JavaScript 程序终止。

2. 执行上下文的入栈与出栈(ECS 的运行过程)

示例代码

function foo() {
  console.log('Inside foo');
}
 
function bar() {
  foo();
  console.log('Inside bar');
}
 
bar();
console.log('End of script');

执行步骤

  1. 全局执行上下文(GEC)创建

    • JavaScript 开始执行时,创建 全局执行上下文(Global Execution Context),并压入 执行上下文栈
  2. bar() 被调用

    • 进入 bar() 函数,创建 bar 的执行上下文,并压入栈顶。
  3. foo() 被调用

    • bar 内部调用 foo(),创建 foo 的执行上下文,并压入栈顶。
  4. 执行 foo() 并出栈

    • foo() 内部执行 console.log('Inside foo')
    • foo() 执行完毕,foo 的执行上下文从栈中弹出。
  5. 执行 bar() 并出栈

    • bar() 继续执行 console.log('Inside bar')
    • bar() 执行完毕,bar 的执行上下文从栈中弹出。
  6. 继续执行全局代码

    • console.log('End of script') 被执行。
    • 全局执行上下文被弹出,执行上下文栈清空,程序执行结束。

3. 可视化执行上下文栈

执行上下文栈的变化如下:

操作执行上下文栈(ECS 状态)
初始状态[ Global Execution Context ]
调用 bar()[ Global Execution Context, bar() ]
调用 foo()[ Global Execution Context, bar(), foo() ]
foo() 执行结束[ Global Execution Context, bar() ]
bar() 执行结束[ Global Execution Context ]
全局执行完毕[](栈清空,代码执行结束)

4. 执行上下文栈的特点

  • 后进先出(LIFO):最新的执行上下文总是在栈顶,执行完毕后弹出。
  • 同步执行:JavaScript 代码是 单线程 执行的,每次只能有一个执行上下文处于运行状态。
  • 函数嵌套管理:每次函数调用都会创建新的执行上下文,并压入栈中,执行完毕后弹出。

5. 异步代码与执行上下文

JavaScript 是 单线程 的,但可以执行异步代码,例如 setTimeoutPromiseasync/await。异步代码不会直接进入执行上下文栈,而是放入 消息队列(Task Queue),等待 事件循环(Event Loop) 将其推入执行栈。

示例

console.log('Start');
 
setTimeout(() => {
  console.log('Timeout callback');
}, 1000);
 
console.log('End');

执行顺序

  1. console.log('Start') 直接执行,输出 "Start"
  2. setTimeout() 注册回调,但不会立即执行,回调被推入 任务队列,主线程继续执行。
  3. console.log('End') 直接执行,输出 "End"
  4. 主线程执行完毕,事件循环从 任务队列 取出 回调 并执行,输出 "Timeout callback"

事件循环与执行上下文的关系

  • 同步任务 立即进入执行上下文栈(ECS)。
  • 异步任务(如 setTimeout)被放入 任务队列(Task Queue),等主线程清空后才会执行。

6. 结论

  • JavaScript 通过执行上下文栈(ECS)管理多个执行上下文,确保函数按照 调用顺序 依次执行,且遵循 后进先出(LIFO) 规则。
  • 全局执行上下文(GEC)始终位于栈底,函数执行上下文在 函数调用时创建并入栈,执行完毕后 出栈
  • 异步任务不会直接进入执行上下文栈,而是存入 任务队列,等到 主线程空闲 后由 事件循环(Event Loop) 推入执行栈。

这样,JavaScript 通过执行上下文栈和事件循环的协作,实现了高效的代码执行管理。 🚀