JavaScript 执行上下文栈

先看代码:

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

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

另一段代码:

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

foo();  // foo2

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

foo(); // foo2

我们都知道这是因为 JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个「准备工作」,比如第一个例子中的变量提升,和第二个例子中的函数提升。

当 JavaScript 碰到可执行代码(executable code)的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做创建执行上下文 1(execution context)

执行上下文栈

在浏览器中的 JavaScript 解释器是单线程的。这实际上意味着,在浏览器中一次只会发生一件事,其他行为或者事件在所谓的执行栈中排队等待。

栈:先进后出,后进先出

我们代码中写了很多的函数, JavaScript 引擎该如何管理这些创建的执行上下文呢?

在 JavaScript 引擎中维护了一个执行上下文栈(Execution context stack,ECS)来管理这些执行上下文;

为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:

ECStack = [];

试想当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个globalContext

ECStack = [
    globalContext
];

现在 JavaScript 遇到下面的这段代码了:

function fn3() {
  console.log('fn3')
}
function fn2() {
  fn3();
}

function fn1() {
  fn2();
}

fn1();

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

// 伪代码

// fn1()
ECStack.push(<fn1> functionContext);

// fn1中竟然调用了 fn2,还要创建 fn2的执行上下文
ECStack.push(<fn2> functionContext);

// 擦,fun2还调用了fn3!
ECStack.push(<fn3> functionContext);

// fun3执行完毕
ECStack.pop();

// fun2执行完毕
ECStack.pop();

// fun1执行完毕
ECStack.pop();

// ...
// JS 接着执行下面的代码,但是 ECStack 底层永远有个 globalContext

参考链接


  1. 叫「执行环境」比较符合我的理解习惯