在执行 JavaScript 代码时,可能会存在多个 执行上下文(Execution Context),JavaScript 引擎通过 执行上下文栈(Execution Context Stack,ECS) 来管理这些上下文。执行上下文栈是一种 后进先出(LIFO,Last In First Out) 的数据结构,它用于存储和管理 JavaScript 代码执行过程中的上下文信息。
1. JavaScript 引擎如何管理执行上下文
JavaScript 引擎的执行流程主要涉及 创建执行上下文、入栈、执行代码、出栈 这几个步骤。以下是详细的管理机制:
(1) 执行上下文栈(ECS)的结构
执行上下文栈类似一个 调用栈,它管理代码的执行顺序。栈中的第一个上下文是 全局执行上下文,后续的函数调用会创建新的执行上下文并压入栈中,而执行完毕后会弹出栈。
栈的管理过程
-
初始化阶段:
- JavaScript 代码开始执行时,会创建 全局执行上下文,并将其压入执行上下文栈(ECS)。
this
绑定到 全局对象(window
或global
)。
-
函数调用阶段:
- 每当 JavaScript 执行一个函数时,都会创建一个新的 函数执行上下文,并将其压入执行上下文栈的顶部。
-
执行完毕出栈:
- 当函数执行完毕后,其对应的执行上下文会从执行上下文栈中弹出,控制权交回上一个执行上下文。
-
整个程序执行完毕:
- 当所有的函数调用都完成后,全局执行上下文最终也会从执行上下文栈中弹出,JavaScript 程序终止。
2. 执行上下文的入栈与出栈(ECS 的运行过程)
示例代码
function foo() {
console.log('Inside foo');
}
function bar() {
foo();
console.log('Inside bar');
}
bar();
console.log('End of script');
执行步骤
-
全局执行上下文(GEC)创建:
- JavaScript 开始执行时,创建 全局执行上下文(Global Execution Context),并压入 执行上下文栈。
-
bar()
被调用:- 进入
bar()
函数,创建bar
的执行上下文,并压入栈顶。
- 进入
-
foo()
被调用:bar
内部调用foo()
,创建foo
的执行上下文,并压入栈顶。
-
执行
foo()
并出栈:foo()
内部执行console.log('Inside foo')
。foo()
执行完毕,foo
的执行上下文从栈中弹出。
-
执行
bar()
并出栈:bar()
继续执行console.log('Inside bar')
。bar()
执行完毕,bar
的执行上下文从栈中弹出。
-
继续执行全局代码:
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 是 单线程 的,但可以执行异步代码,例如 setTimeout、Promise、async/await。异步代码不会直接进入执行上下文栈,而是放入 消息队列(Task Queue),等待 事件循环(Event Loop) 将其推入执行栈。
示例
console.log('Start');
setTimeout(() => {
console.log('Timeout callback');
}, 1000);
console.log('End');
执行顺序
console.log('Start')
→ 直接执行,输出"Start"
。setTimeout()
注册回调,但不会立即执行,回调被推入 任务队列,主线程继续执行。console.log('End')
→ 直接执行,输出"End"
。- 主线程执行完毕,事件循环从 任务队列 取出 回调 并执行,输出
"Timeout callback"
。
事件循环与执行上下文的关系
- 同步任务 立即进入执行上下文栈(ECS)。
- 异步任务(如
setTimeout
)被放入 任务队列(Task Queue),等主线程清空后才会执行。
6. 结论
- JavaScript 通过执行上下文栈(ECS)管理多个执行上下文,确保函数按照 调用顺序 依次执行,且遵循 后进先出(LIFO) 规则。
- 全局执行上下文(GEC)始终位于栈底,函数执行上下文在 函数调用时创建并入栈,执行完毕后 出栈。
- 异步任务不会直接进入执行上下文栈,而是存入 任务队列,等到 主线程空闲 后由 事件循环(Event Loop) 推入执行栈。
这样,JavaScript 通过执行上下文栈和事件循环的协作,实现了高效的代码执行管理。 🚀