JS 执行上下文栈

avatar
Ctrl+C、V工程师 @豌豆公主

这篇文章是一个学习笔记,先以一个宏观的角度考虑JS 引擎的执行过程,方便之后学习执行上下文。

JS 是怎么执行代码的

与大家想的可能有些不太一样,JS 代码的执行并非是按照顺序一行一行执行的。而是以为单位来执行的。 那么问题来了,这个 是啥?JS 引擎是怎么区分这些的?

JS 可执行代码

先回答第一个问题,上文说的有三种:

  1. 全局代码
  2. 函数代码
  3. eval代码

这三种代码都是我们所谓的。而每次 JS 引擎遇到这种块,就会进行分析,这个过程也就是创建执行上下文(执行环境)的过程。

执行上下文栈

我们知道,一个完整的程序,也许是充满了各种函数(先不不考虑变量),换句话说,完整的程序中可能会有很多的执行上下文。为了方便 JS 引擎进行管理,需要有一个栈来管理这些执行上下文,这个栈就是执行上下文栈(ECStack => Execution content Stack)。

现在可以解释一下第二个问题。JS 引擎在创建执行上下文的时候,会默认在栈(ECStack)的底部,先 push 一个全局上下文,因为每个程序都有一个全局代码,接下来每识别到一个函数,也就是函数代码,就会创建一个执行上下文,并继续 push 到 栈中。

入栈规则

  1. 一个完整的程序在执行过程中,一开始入栈的一定是全局上下文,因此栈的底部就是 globalContext
  2. 接下来每遇到一个被执行函数,就会创建一个执行上下文。
  3. 如果前一个函数中还有函数被执行,会继续push新的执行上下文。
  4. 当函数执行完成之后,会把刚执行完的上下文进行pop操作。

Demo1

function fun3() { console.log('fun3'); }
function fun2() { fun3(); }
function fun1() { fun2(); }
fun1();

分析:

  1. 按照规则,栈的底部一定是一个全局上下文。
ECStack.push(globalContext);
  1. 遇到第一个执行的函数是fun1,入栈。
ECStack.push(fun1);
  1. fun1上下文中执行的过程中,fun2被执行,入栈。
ECStack.push(fun2);
  1. fun2上下文中执行的过程中,fun3被执行,入栈。
ECStack.push(fun3);
  1. fun3执行完毕,出栈。
ECStack.pop();
ECStack.length == 3
  1. fun2执行完毕,出栈。
ECStack.pop();
ECStack.length == 2
  1. fun1执行完毕,出栈。
ECStack.pop();
ECStack.length == 1
  1. 程序结束,全局上下文出栈。
ECStack.pop();
ECStack.length == 0

Demo2

在开始前,可以先考虑一下这两个片段入栈的情况。

// ------------ 片段1 ------------
function fun1() { 
  var value = 'scope';
  function fun2() {
    return value;
  }
  return fun2();
}
fun1();
// ------------ 片段2 ------------
function fun1() { 
  var value = 'scope';
  function fun2() {
    return value;
  }
  return fun2;
}
fun1()();

片段 1

偷懒了,画图好麻烦。省略号表示一直出栈直到清空。

  1. 按照规则,栈的底部一定是一个全局上下文。
ECStack.push(globalContext);
  1. 遇到第一个执行的函数是fun1,入栈。
ECStack.push(fun1);
  1. fun1执行return的时候,fun2被执行,入栈。
ECStack.push(fun2);
  1. fun2执行结束,出栈。
ECStack.pop();
  1. fun1执行结束,出栈。
ECStack.pop();
  1. 全部执行结束,globalContext出栈。
ECStack.pop();

片段 2

  1. 按照规则,栈的底部一定是一个全局上下文。
ECStack.push(globalContext);
  1. 遇到第一个执行的函数是fun1,入栈。
ECStack.push(fun1);
  1. fun1执行return的时候,fun2被执行,返回函数,fun1出栈。
ECStack.pop();
  1. fun2执行,入栈。
ECStack.push(fun2);
  1. fun2执行结束,出栈。
ECStack.pop();
  1. 全部执行结束,globalContext出栈。
ECStack.pop();

总结

  • 函数被执行的时候才会创建执行上下文。
  • 一个函数一但执行完毕,就会执行出栈操作。
  • 一定会有一个全局上下文在栈的底部。