深入 JavaScript 执行上下文栈

120 阅读3分钟

欢迎关注微信公众号:前端阅读室

顺序执行?

我们对 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

却发现打印的结果都是 foo2。这是什么原因呢?

原因是:JavaScript 引擎执行是顺序执行的,但是它并不是一行一行地分析和执行代码,而是一段一段地分析和执行的。当执行每一段代码之前,它会先做一个"准备工作"。比如第一个例子,涉及到了变量声明的提升。第二个例子,涉及到了函数声明的整体提升。

我们这节课的目的是希望大家弄清楚 JavaScript 引擎遇到一段怎样的代码时才会做"准备工作",以及这"一段一段"代码究竟是如何执行的。

可执行代码

在 JavaScript 中,可执行代码(executable code)一共分为三类:全局代码、函数代码、eval 代码。

JavaScript 执行到一个函数时,就会进行"准备工作",用一个更专业的说法就是创建"执行上下文(execution context)"

执行上下文栈

那么问题来了,我们写的函数多了去了,JavaScript 引擎如何创建和管理这么多执行上下文呢?

答案是:JavaScript 引擎通过创建执行上下文栈(Execution context stack,ECS)来管理执行上下文

我们通过一段代码来讲解下 JavaScript 是如何通过执行上下文栈来管理执行上下文的。

代码如下:

function fun3() {
  console.log("fun3");
}

function fun2() {
  fun3();
}

function fun1() {
  fun2();
}

fun1();

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

ECStack = [];

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

ECStack = [globalContext];

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

// 伪代码

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

// fun1 中调用了 fun2,需要创建 fun2 的执行上下文,并压入执行上下文栈。
ECStack.push(<fun2> functionContext);

// fun2 还调用了 fun3,同理,创建 fun3 的执行上下文,并压入执行上下文栈。
ECStack.push(<fun3> functionContext);

// fun3 执行完毕,fun3 执行上下文弹出。
ECStack.pop();

// fun2 执行完毕,fun2 执行上下文弹出。
ECStack.pop();

// fun1 执行完毕,fun1 执行上下文弹出。
ECStack.pop();

// 如果下面还有代码,JavaScript 会接着执行下面的代码,此时 ECStack 底层还有个globalContext。

解答上一节课的思考题

在上一节深入 JavaScript 词法作用域的课中我们留下了一道思考题,让大家思考一下下面这两段代码在执行过程中有什么不同?

var scope = "global scope";
function checkscope() {
  var scope = "local scope";
  function f() {
    return scope;
  }
  return f();
}
checkscope();
var scope = "global scope";
function checkscope() {
  var scope = "local scope";
  function f() {
    return scope;
  }
  return f;
}
checkscope()();

首先,这两段代码返回的结果都是"local scope",不同之处在于它们的执行上下文栈的变化是不一样的。

第一段代码:

ECStack.push(globalContext);
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();// <f> functionContext
ECStack.pop();// <checkscope> functionContext

第二段代码:

ECStack.push(globalContext);
ECStack.push(<checkscope> functionContext);
ECStack.pop();// <checkscope> functionContext
ECStack.push(<f> functionContext);
ECStack.pop();// <f> functionContext

当然了,本节仅仅是讲解了执行上下文栈的变化,并没有详细讲解执行上下文到底包含了哪些内容,这部分我会在下一节介绍。

欢迎关注微信公众号:前端阅读室