【JavaScript】执行栈和执行上下文

95 阅读5分钟

1. 执行上下文(Execution Context)

执行上下文是 JavaScript 中代码执行的环境的抽象概念,它包含了代码运行时所需的所有信息,比如变量的值、函数的引用等。

每当 JavaScript 代码执行前,都会创建一个执行上下文,并将其添加到执行栈中。执行上下文负责代码的执行和管理作用域。

JavaScript 中有三种执行上下文类型(环境)。

  • 全局执行上下文:这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。他会执行两件事,创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。
  • 函数执行上下文:每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
  • Eval 函数执行上下文:执行在 eval 函数内部的代码也会有它属于自己的执行上下文。

执行上下文包含三个重要的属性:

  • 变量对象(Variable Object):包含变量、函数声明和形参等。
  • 作用域链(Scope Chain):用于解析标识符的链表。
  • this 指向:指向函数执行时的上下文对象。

JavaScript 运行时首先会进入全局环境,对应会生成全局上下文。程序代码中基本都会存在函数,那么调用函数,就会进入函数执行环境,对应就会生成该函数的执行下文。

由于代码中会声明多个函数,对应的函数执行上下文也会存在多个。在JavaScript中,通过栈的存取方式来管理执行上下文,我们可称其为执行栈,或函数调用栈(Call Stack)。

const foo = function (i) {
    console.log(b)  // undefined
    console.log(c)  // [Function: c]
    let a = 'hello'
    let b = function privateB() {}
    function c(){}
}
foo(10)

// 1. 创建上下文阶段

// vo 里面确定的东西:
// 1. 函数的形参(并赋值)
// 2. 函数的 arguments 对象(并赋值)
// 3. 确定普通字面量形式的函数声明(并赋值)
// 3. 变量声明、函数表达式声明(未赋值)

// 将执行上下文看做是一个对象
// fooExecutionContext = {
    // vo = {
    //     i:  10,
    //     arguments: {0: 10, length: 1},
    //     c: 指向 c 那个函数,
    //     a: undefined,
    //     b: undefined
    // },
    // this,
    // scope
// }

// 2. 执行代码阶段
fooExecutionContext = {
    vo = {
        i:  10,
        arguments: {0: 10, length: 1},
        c: 指向 c 那个函数,
        a: 'hello',
        b: privateB
    },
    this,
    scope
}

2. 栈(Stack)

执行栈是一个后进先出(LIFO)的数据结构,用于存储执行上下文。当 JavaScript 代码执行时,会创建执行上下文,并将其推入执行栈的顶部。当函数执行完成后,对应的执行上下文将从栈顶弹出,控制权返回到上一个执行上下文。

执行栈(函数调用栈)

理解完栈的存取方式,我们接着分析 JavaScript 中如何通过栈来管理多个执行上下文。

程序执行会先创建全局执行上下文,所以栈底都是全局执行上下文;而栈顶是正在执行函数的执行上下文。

执行上下文可存在多个,虽然没有明确的数量限制,但如果超出栈分配的空间,会造成堆栈溢出。常见于递归调用,没有终止条件造成死循环的场景。

  • 当脚本要调用一个函数时,解析器把该函数添加到栈中并且执行这个函数。
  • 任何被这个函数调用的函数会进一步添加到调用栈中,并且运行到它们被上个程序调用的位置。
  • 当函数运行结束后,解释器将它从堆栈中取出,并在主代码列表中继续执行代码。
  • 如果栈占用的空间比分配给它的空间还大,那么则会导致“栈溢出”错误。

3. 执行上下文生命周期

执行上下文的生命周期包括以下几个阶段:

  • 创建阶段(Creation Phase):在此阶段,JavaScript 引擎会创建执行上下文。在全局上下文中,这一阶段发生在代码执行前;在函数上下文中,这一阶段发生在函数被调用时。
    • 初始化变量对象(Variable Object / VO)
      • 确定函数形参并赋值
      • 函数环境初始化创建arguments对象并赋值
      • 确定普通字面量形式的函数声明并赋值
      • 变量声明,函数表达式声明(未赋值)
    • 建立作用域链(Scope Chain)(在声明定义的地方确定)
    • 确定 this 的指向(this 由调用者确定)。
  • 执行阶段(Execution Phase):在此阶段,JavaScript 引擎会按照代码的顺序执行相应的代码,并为变量赋值、函数调用等。在执行阶段中,JavaScript 引擎会根据作用域链解析变量和函数标识符,并执行相应的代码逻辑。
  • 销毁阶段(Deletion Phase):在函数执行完成后或全局代码执行完成后,对应的执行上下文会被销毁。JavaScript 引擎会从执行栈中弹出该执行上下文,并释放相关的内存资源。
(function () {
    console.log(typeof foo);
    console.log(typeof bar);
    var foo = "Hello";
    var bar = function A() {
        return "World";
    }

    function foo() {
        return "good";
    }

    console.log(foo, typeof foo);
})()
// 1. 创建上下文阶段
// exeutionContext = {
//     vo: {
//         foo: 指向 foo 函数,
//         // 已有 foo 不会创建 foo 变量了
//         bar: undefined
//     },
//     this,
//     scope
// }
// 2. 执行代码
exeutionContext = {
    vo: {
        foo: "Hello",
        // 已有 foo 不会创建 foo 变量了
        bar: function A
    },
    this,
    scope
}

console.log(typeof foo);  // function
console.log(typeof bar);  // undefined
var foo = "Hello";
var bar = function A() {
    return "World";
}

function foo() {
    return "good";
}

console.log(foo, typeof foo); //hello, string