Skip to content

JavaScript 执行栈与执行上下文


1. 什么是执行上下文 (Execution Context)?

执行上下文 (Execution Context) 是 JavaScript 代码在执行前所需创建的准备环境。任何代码的执行都必须在某个上下文中进行。

  • 英文全称: Execution Context
  • 核心作用: 为即将执行的代码(如全局代码、函数代码)提供必要的环境和资源。
  • 创建时机: 当 JavaScript 引擎准备执行一段代码时,会先进入对应的执行环境,并为该环境创建一个执行上下文。
    • 全局代码: 在执行全局代码前,创建 全局执行上下文
    • 函数代码: 在 调用 函数时(而非定义时),创建 函数执行上下文

1.1 执行上下文的类型

JavaScript 中主要有三种执行环境,对应三种执行上下文:

  1. 全局执行上下文 (Global Execution Context)
    • 这是最基础的上下文,代码开始执行时首先会进入全局环境,并创建全局执行上下文。
    • 一个程序中只会有一个全局执行上下文。
  2. 函数执行上下文 (Function Execution Context)
    • 每当一个函数被 调用 时,就会为该函数创建一个新的执行上下文。
    • 每个函数调用都会产生一个独一无二的执行上下文。
  3. Eval 函数执行上下文 (已不推荐使用)
    • 执行 eval() 函数内部的代码时会创建。

2. 什么是执行栈 (Execution Stack)?

由于代码中可能调用多个函数,从而产生多个函数执行上下文,JavaScript 引擎使用 栈 (Stack) 这种数据结构来管理这些上下文。这个栈通常被称为 执行栈调用栈 (Call Stack)

2.1 栈的数据结构

  • 特性: 类似于一个只有一个开口的乒乓球桶,遵循 先进后出,后进先出 (LIFO - Last-In, First-Out) 的原则。
  • 操作:
    • 入栈 (Push): 将数据放入栈顶。
    • 出栈 (Pop): 从栈顶取出数据。

2.2 执行栈的工作流程

  1. 全局上下文入栈: JS 引擎开始执行代码时,首先将 全局执行上下文 压入执行栈底。它始终位于栈底,只有当整个应用程序结束时才会出栈。
  2. 函数上下文入栈: 每当调用一个函数时,会为该函数创建一个新的 函数执行上下文,并将其压入栈顶。此时,栈顶的上下文成为当前 活动上下文 (Active Context),引擎会执行该上下文中的代码。
  3. 函数上下文出栈: 当栈顶的函数执行完毕后(例如执行到 return 或函数结束),其对应的函数执行上下文就会从栈顶 出栈 (Pop),控制权交还给栈中下一个上下文。
  4. 循环往复: 重复步骤 2 和 3,直到所有代码执行完毕。
  5. 全局上下文出栈: 当浏览器窗口关闭或程序退出时,全局执行上下文最终出栈。

示例代码与图解

javascript
function bar() {
    console.log('bar function');
}

function foo() {
    console.log('foo function');
    bar();
}

foo();

执行流程分析:

  1. Global Context 入栈
  2. 执行代码,调用 foo()
  3. foo 的执行上下文 入栈。引擎开始执行 foo 内部代码。
  4. foo 内部调用 bar()
  5. bar 的执行上下文 入栈。引擎开始执行 bar 内部代码。
  6. bar 执行完毕,bar 的执行上下文 出栈。
  7. foo 执行完毕,foo 的执行上下文 出栈。
  8. 程序结束,Global Context 出栈。

2.3 堆栈溢出 (Stack Overflow)

执行栈的容量是有限的。如果入栈的执行上下文数量超过了栈的容量,就会导致 堆栈溢出 (Stack Overflow)。这通常发生在没有终止条件的递归调用中。

javascript
function recursiveFunc() {
    recursiveFunc(); // 无限递归调用
}

recursiveFunc(); // Uncaught RangeError: Maximum call stack size exceeded

3. 执行上下文的生命周期

执行上下文的生命周期分为两个主要阶段:

  1. 创建阶段 (Creation Phase)
  2. 执行阶段 (Execution Phase)

重点: 创建阶段 是理解变量提升、this 指向等核心概念的关键。在这一阶段,代码还未执行,引擎正在做准备工作。

3.1 创建阶段 (Creation Phase)

当一个函数被调用但其内部代码尚未执行时,引擎会进入创建阶段,并完成三件重要的事情:

  1. 创建变量对象 (Variable Object, VO)
  2. 建立作用域链 (Scope Chain)
  3. 确定 this 的指向

我们可以将执行上下文本身看作一个对象 (ExecutionContext Object),它包含以下三个核心属性:

javascript
ExecutionContext = {
  variableObject: { /* ... */ },  // 简称 VO
  scopeChain:     { /* ... */ },
  this:           { /* ... */ }
}

3.2 变量对象 (Variable Object, VO)

变量对象是一个与执行上下文相关的特殊对象,用于存储上下文中定义的 变量函数声明函数参数。在创建阶段,引擎会扫描当前上下文的代码,并按以下顺序填充变量对象:

  1. 函数参数 (Arguments): 创建 arguments 对象,并初始化。同时,将函数传入的形参作为 VO 的属性,属性值为实参的值。
  2. 函数声明 (Function Declarations): 扫描所有通过 function funcName() {} 形式声明的函数,将其函数名作为 VO 的属性,属性值为函数对象本身。如果 VO 中已存在同名属性,则会覆盖它
  3. 变量声明 (Variable Declarations): 扫描所有通过 var 声明的变量,将其变量名作为 VO 的属性,并初始化为 undefined如果 VO 中已存在同名属性(例如,一个同名的函数声明),则该变量声明将被忽略,不会覆盖已有属性

这就是 变量提升 (Hoisting)函数提升 的根本原因。

3.3 执行阶段 (Execution Phase)

创建阶段结束后,引擎进入执行阶段。此时,代码会从上到下一行一行地执行。在执行过程中,引擎会完成变量赋值、代码执行等操作,并根据代码的执行情况来修改变量对象中的值。


4. 案例分析

通过一个经典的面试题来深入理解执行上下文的创建和执行过程。

javascript
(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) 是如何形成的:

  1. 处理函数参数: 该函数没有参数,跳过。
  2. 处理函数声明:
    • 找到 function foo() {}
    • 在 VO 中创建一个属性 foo,其值指向该函数对象。
    • VO = { foo: <Function> }
  3. 处理变量声明:
    • 找到 var foo = 'hello';
      • 检查 VO 中是否存在 foo 属性。存在!因此 忽略 var foo 这个声明。VO 不变。
    • 找到 var bar = function() { ... };
      • 检查 VO 中是否存在 bar 属性。不存在。
      • 在 VO 中创建一个属性 bar,并将其值初始化为 undefined
    • 最终,创建阶段结束时的 VO 如下:
    javascript
    VO = {
      foo: <Function: foo>, // 函数声明提升,优先级更高
      bar: undefined        // 变量声明提升
    }

步骤 2: 执行阶段

现在,代码开始从上到下执行:

  1. console.log(typeof foo);
    • 在 VO 中查找 foo,找到一个函数。
    • 输出: function
  2. console.log(typeof bar);
    • 在 VO 中查找 bar,找到 undefined
    • 输出: undefined
  3. var foo = 'hello';
    • 这是一条赋值语句。将字符串 'hello' 赋给 VO 中的 foo 属性。
    • VO.foo 的值从 <Function: foo> 变为 'hello'
  4. var bar = function() { ... };
    • 这是一条赋值语句。将一个匿名函数表达式赋给 VO 中的 bar 属性。
    • VO.bar 的值从 undefined 变为 <Function>
  5. function foo() { ... }
    • 在执行阶段,函数声明会被跳过,因为它已在创建阶段处理完毕。
  6. console.log(foo);
    • 在 VO 中查找 foo,其当前值为 'hello'
    • 输出: hello
  7. console.log(typeof foo);
    • 查找 foo 的类型,其当前值为 'hello'
    • 输出: string

最终结果

function
undefined
hello
string