JavaScript 执行栈与执行上下文
1. 什么是执行上下文 (Execution Context)?
执行上下文 (Execution Context) 是 JavaScript 代码在执行前所需创建的准备环境。任何代码的执行都必须在某个上下文中进行。
- 英文全称:
Execution Context - 核心作用: 为即将执行的代码(如全局代码、函数代码)提供必要的环境和资源。
- 创建时机: 当 JavaScript 引擎准备执行一段代码时,会先进入对应的执行环境,并为该环境创建一个执行上下文。
- 全局代码: 在执行全局代码前,创建 全局执行上下文。
- 函数代码: 在 调用 函数时(而非定义时),创建 函数执行上下文。
1.1 执行上下文的类型
JavaScript 中主要有三种执行环境,对应三种执行上下文:
- 全局执行上下文 (Global Execution Context)
- 这是最基础的上下文,代码开始执行时首先会进入全局环境,并创建全局执行上下文。
- 一个程序中只会有一个全局执行上下文。
- 函数执行上下文 (Function Execution Context)
- 每当一个函数被 调用 时,就会为该函数创建一个新的执行上下文。
- 每个函数调用都会产生一个独一无二的执行上下文。
- Eval 函数执行上下文 (已不推荐使用)
- 执行
eval()函数内部的代码时会创建。
- 执行
2. 什么是执行栈 (Execution Stack)?
由于代码中可能调用多个函数,从而产生多个函数执行上下文,JavaScript 引擎使用 栈 (Stack) 这种数据结构来管理这些上下文。这个栈通常被称为 执行栈 或 调用栈 (Call Stack)。
2.1 栈的数据结构
- 特性: 类似于一个只有一个开口的乒乓球桶,遵循 先进后出,后进先出 (LIFO - Last-In, First-Out) 的原则。
- 操作:
- 入栈 (Push): 将数据放入栈顶。
- 出栈 (Pop): 从栈顶取出数据。
2.2 执行栈的工作流程
- 全局上下文入栈: JS 引擎开始执行代码时,首先将 全局执行上下文 压入执行栈底。它始终位于栈底,只有当整个应用程序结束时才会出栈。
- 函数上下文入栈: 每当调用一个函数时,会为该函数创建一个新的 函数执行上下文,并将其压入栈顶。此时,栈顶的上下文成为当前 活动上下文 (Active Context),引擎会执行该上下文中的代码。
- 函数上下文出栈: 当栈顶的函数执行完毕后(例如执行到
return或函数结束),其对应的函数执行上下文就会从栈顶 出栈 (Pop),控制权交还给栈中下一个上下文。 - 循环往复: 重复步骤 2 和 3,直到所有代码执行完毕。
- 全局上下文出栈: 当浏览器窗口关闭或程序退出时,全局执行上下文最终出栈。
示例代码与图解
function bar() {
console.log('bar function');
}
function foo() {
console.log('foo function');
bar();
}
foo();执行流程分析:
Global Context入栈。- 执行代码,调用
foo()。 foo的执行上下文 入栈。引擎开始执行foo内部代码。foo内部调用bar()。bar的执行上下文 入栈。引擎开始执行bar内部代码。bar执行完毕,bar的执行上下文 出栈。foo执行完毕,foo的执行上下文 出栈。- 程序结束,
Global Context出栈。
2.3 堆栈溢出 (Stack Overflow)
执行栈的容量是有限的。如果入栈的执行上下文数量超过了栈的容量,就会导致 堆栈溢出 (Stack Overflow)。这通常发生在没有终止条件的递归调用中。
function recursiveFunc() {
recursiveFunc(); // 无限递归调用
}
recursiveFunc(); // Uncaught RangeError: Maximum call stack size exceeded3. 执行上下文的生命周期
执行上下文的生命周期分为两个主要阶段:
- 创建阶段 (Creation Phase)
- 执行阶段 (Execution Phase)
重点: 创建阶段 是理解变量提升、
this指向等核心概念的关键。在这一阶段,代码还未执行,引擎正在做准备工作。
3.1 创建阶段 (Creation Phase)
当一个函数被调用但其内部代码尚未执行时,引擎会进入创建阶段,并完成三件重要的事情:
- 创建变量对象 (Variable Object, VO)
- 建立作用域链 (Scope Chain)
- 确定
this的指向
我们可以将执行上下文本身看作一个对象 (
ExecutionContext Object),它包含以下三个核心属性:javascriptExecutionContext = { variableObject: { /* ... */ }, // 简称 VO scopeChain: { /* ... */ }, this: { /* ... */ } }
3.2 变量对象 (Variable Object, VO)
变量对象是一个与执行上下文相关的特殊对象,用于存储上下文中定义的 变量、函数声明 和 函数参数。在创建阶段,引擎会扫描当前上下文的代码,并按以下顺序填充变量对象:
- 函数参数 (Arguments): 创建
arguments对象,并初始化。同时,将函数传入的形参作为 VO 的属性,属性值为实参的值。 - 函数声明 (Function Declarations): 扫描所有通过
function funcName() {}形式声明的函数,将其函数名作为 VO 的属性,属性值为函数对象本身。如果 VO 中已存在同名属性,则会覆盖它。 - 变量声明 (Variable Declarations): 扫描所有通过
var声明的变量,将其变量名作为 VO 的属性,并初始化为undefined。如果 VO 中已存在同名属性(例如,一个同名的函数声明),则该变量声明将被忽略,不会覆盖已有属性。
这就是 变量提升 (Hoisting) 和 函数提升 的根本原因。
3.3 执行阶段 (Execution Phase)
创建阶段结束后,引擎进入执行阶段。此时,代码会从上到下一行一行地执行。在执行过程中,引擎会完成变量赋值、代码执行等操作,并根据代码的执行情况来修改变量对象中的值。
4. 案例分析
通过一个经典的面试题来深入理解执行上下文的创建和执行过程。
(function() {
console.log(typeof foo); // ?
console.log(typeof bar); // ?
var foo = 'hello';
var bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
console.log(foo); // ?
console.log(typeof foo); // ?
})();步骤 1: 创建阶段
当这个立即执行函数 (IIFE) 被调用时,会立即为其创建一个函数执行上下文。我们来分析其 创建阶段 的 Variable Object (VO) 是如何形成的:
- 处理函数参数: 该函数没有参数,跳过。
- 处理函数声明:
- 找到
function foo() {}。 - 在 VO 中创建一个属性
foo,其值指向该函数对象。 VO = { foo: <Function> }
- 找到
- 处理变量声明:
- 找到
var foo = 'hello';。- 检查 VO 中是否存在
foo属性。存在!因此 忽略var foo这个声明。VO 不变。
- 检查 VO 中是否存在
- 找到
var bar = function() { ... };。- 检查 VO 中是否存在
bar属性。不存在。 - 在 VO 中创建一个属性
bar,并将其值初始化为undefined。
- 检查 VO 中是否存在
- 最终,创建阶段结束时的 VO 如下:
javascriptVO = { foo: <Function: foo>, // 函数声明提升,优先级更高 bar: undefined // 变量声明提升 } - 找到
步骤 2: 执行阶段
现在,代码开始从上到下执行:
console.log(typeof foo);- 在 VO 中查找
foo,找到一个函数。 - 输出:
function
- 在 VO 中查找
console.log(typeof bar);- 在 VO 中查找
bar,找到undefined。 - 输出:
undefined
- 在 VO 中查找
var foo = 'hello';- 这是一条赋值语句。将字符串
'hello'赋给 VO 中的foo属性。 VO.foo的值从<Function: foo>变为'hello'。
- 这是一条赋值语句。将字符串
var bar = function() { ... };- 这是一条赋值语句。将一个匿名函数表达式赋给 VO 中的
bar属性。 VO.bar的值从undefined变为<Function>。
- 这是一条赋值语句。将一个匿名函数表达式赋给 VO 中的
function foo() { ... }- 在执行阶段,函数声明会被跳过,因为它已在创建阶段处理完毕。
console.log(foo);- 在 VO 中查找
foo,其当前值为'hello'。 - 输出:
hello
- 在 VO 中查找
console.log(typeof foo);- 查找
foo的类型,其当前值为'hello'。 - 输出:
string
- 查找
最终结果
function
undefined
hello
string