深入 JS 之执行上下文栈

177 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

顺序执行?

在刚刚学习 JavaScript 的时候, 我们都会被告知, JS 的代码是顺序执行的, 自上到下, 逐行执行. 但是当我们深入一些学习之后, 或者说我们去刷过 JS 的执行顺序面试题,就会知道, JS好像也并不是逐行分析逐行执行啊, 就比如下面这个例子

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

foo();  // foo2

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

foo(); // foo2

为啥不是 foo1foo2 呢?

其实 JS 引擎并不是一行一行的解释执行代码, 而是一段段的执行, 当执行一段代码的时候. 会进行一些 "准备工作". 例如: 上面例子中的函数提升

可执行代码

JS 中, 可执行代码(executable code)分为

  • 全局代码
  • 函数代码
  • eval 代码

在一段可执行代码要被执行的时候,就会进行一些准备工作, 用专业的说法就是, 创建 执行上下文(execution context).

执行上下文栈

全局代码就一个份, eval 使用非常少, 都还好说, 在任何一个程序中, 函数可是多的数都数不清, 每一个函数执行都需要创建 执行上下文, 那么如何管理这些上下文信息呢?

所以 JS 引擎创建了 执行上下文栈(Execution context stack, ECS) 用来管理上下文

栈: 一种先进后出后进先出的数据结构

接下来让我们来模拟一下执行上下文栈的行为, 来帮助理解

这里我们采用数组来模拟

const ECStack = [];

JS 要执行代码的时候, 最先执行的肯定是全局代码, 所以初始化的时候, 首先会向栈里放入 全局执行上下文, 这里我们采用 globalContext 来表示, 我们要知道,全局执行上下文会一直在栈底, 直到应用程序结束的时候, 才会被清空

const ECStack = [
  globalContext
]

接下来我们遇到以下 可执行代码

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

当执行一个函数的时候,就会创建一个执行上下文, 并压入执行上下文栈中, 当函数执行完毕之后, 就会将函数执行上下文从栈中弹出. 知道了这样的工作原理,让我们来看看如何处理上面这段代码:

// 执行 fun1
ECStack.push(<fun1> functionContext);
// 执行 fun2
ECStack.push(<fun2> functionContext);
// 执行 fun3
ECStack.push(<fun3> functionContext);
// 执行完毕
ECStack.pop(); // fun3执行完毕 出栈
ECStack.pop(); // fun2执行完毕 出栈
ECStack.pop(); // fun1执行完毕 出栈
// 然后去执行别的代码, 全局上下文除了应用结束,否则是不会出栈的

我们来看下动画的执行

execution-context-stack