在 JavaScript 中,代码的执行环境和执行活动栈(Execution Context and Call Stack)是理解 JavaScript 运行机制的核心概念。它们决定了代码的执行顺序、作用域、变量访问等行为。

示意图

//试着画出下面代码的执行逻辑
let a = 123
function abc() {
    let a = 100
    return function (x) {
        a += x
        console.log(a);
    }
}
const c = abc()
c(200) //输出 300
c(600) //输出 900
const d = abc()  
d(666) //输出 766
  • 每次调用 abc() 都会创建一个新的闭包,闭包中的变量 a 是独立的。
  • cd 是两个不同的闭包,它们内部的变量 a 互不影响。
  • 输出结果分别为 300900766

代码执行环境

1. 执行环境(Execution Context)

执行环境是 JavaScript 代码执行时的抽象概念,它包含了当前代码执行所需的所有信息。每当 JavaScript 引擎执行一段代码时,都会创建一个执行环境。

执行环境的类型:

  1. 全局执行环境(Global Execution Context)

    • 这是最外层的执行环境,对应全局作用域。

    • 在浏览器中,全局执行环境的 this 指向 window 对象。

    • 全局执行环境在脚本加载时创建,直到页面关闭时销毁。

  2. 函数执行环境(Function Execution Context)

    • 每次调用函数时,都会创建一个新的函数执行环境。

    • 函数执行环境包含了函数的局部变量、参数、作用域链等信息。

    • 函数执行完成后,其执行环境会被销毁。

  3. Eval 执行环境(Eval Execution Context)

    • 使用 eval 函数时创建的执行环境(不推荐使用 eval,因此很少讨论)。

2. 执行环境的组成

每个执行环境包含以下三个部分:

  1. 变量对象(Variable Object, VO)

    • 存储当前环境中定义的变量、函数声明和函数参数。

    • 在全局环境中,变量对象是全局对象(如 window)。

    • 在函数环境中,变量对象是活动对象(Activation Object, AO)。

  2. 作用域链(Scope Chain)

    • 作用域链是一个链表,用于解析变量和函数。

    • 当访问一个变量时,JavaScript 引擎会沿着作用域链从当前环境向上查找,直到找到该变量或到达全局环境。

  3. this 值

    • this 指向当前执行环境的上下文对象。

    • 在全局环境中,this 指向全局对象(如 window)。

    • 在函数环境中,this 的值取决于函数的调用方式。


3. 执行活动栈(Call Stack)

执行活动栈(也称为调用栈)是一个后进先出(LIFO)的栈结构,用于管理执行环境的创建和销毁。

执行活动栈的工作流程:

  1. 初始状态

    • 当 JavaScript 脚本开始执行时,全局执行环境被创建并推入调用栈。
  2. 函数调用

    • 当调用一个函数时,会创建一个新的函数执行环境,并将其推入调用栈。

    • 函数执行完成后,其执行环境从调用栈中弹出。

  3. 栈溢出

    • 如果递归调用过深或函数调用过多,调用栈可能会超出其最大限制,导致栈溢出错误(Stack Overflow)。

示例:

function first() {
    console.log("First");
    second();
}

function second() {
    console.log("Second");
    third();
}

function third() {
    console.log("Third");
}

first();

调用栈的变化:

  1. 全局执行环境被推入调用栈。

  2. 调用 first()first 的执行环境被推入调用栈。

  3. 在 first 中调用 second()second 的执行环境被推入调用栈。

  4. 在 second 中调用 third()third 的执行环境被推入调用栈。

  5. third 执行完成后,其执行环境从调用栈中弹出。

  6. second 执行完成后,其执行环境从调用栈中弹出。

  7. first 执行完成后,其执行环境从调用栈中弹出。

  8. 最后,全局执行环境从调用栈中弹出(脚本执行完毕)。


4. 执行环境和作用域链的关系

作用域链是执行环境的一个重要组成部分,它决定了变量的可访问性。

示例:

const globalVar = "Global";

function outer() {
    const outerVar = "Outer";

    function inner() {
        const innerVar = "Inner";
        console.log(globalVar); // 输出: Global
        console.log(outerVar); // 输出: Outer
        console.log(innerVar); // 输出: Inner
    }

    inner();
}

outer();

作用域链:

  • inner 函数的作用域链:inner → outer → 全局。

  • 当 inner 访问 globalVar 时,JavaScript 引擎会沿着作用域链向上查找,直到找到 globalVar


5. 闭包与执行环境

闭包是函数与其词法作用域的结合。即使函数在其词法作用域外执行,它仍然可以访问其词法作用域中的变量。

示例:

function outer() {
    const outerVar = "Outer";

    function inner() {
        console.log(outerVar); // 输出: Outer
    }

    return inner;
}

const closure = outer();
closure();

解释:

  • inner 函数形成了一个闭包,它“记住”了 outer 函数的词法作用域。

  • 即使 outer 函数已经执行完毕,inner 仍然可以访问 outerVar


总结:

  • 执行环境:是 JavaScript 代码执行时的抽象概念,包含变量对象、作用域链和 this 值。

  • 执行活动栈:是一个后进先出的栈结构,用于管理执行环境的创建和销毁。

  • 作用域链:决定了变量的可访问性,沿着作用域链从当前环境向上查找变量。

  • 闭包:是函数与其词法作用域的结合,即使函数在其词法作用域外执行,仍然可以访问其词法作用域中的变量。