JavaScript之执行上下文栈

170 阅读5分钟

顺序执行?

我们先来看下面这两段代码

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 引擎遇到一段怎么样的代码才会做“准备工作”呢?

可执行代码

这就要说到 JavaScript 的可执行(executable code)的类型有哪些了?

其实很简单,就三种:全局代码、函数代码、eval代码

举个例子,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(execution context)"。

执行上下文的类型也分三种:

全局执行上下文:它做了两件事,1.创建一个全局对象,在浏览器中这个全局对象就是 window 对象。 2.将 this 指针指向这个全局对象。 一个程序只能存在一个全局执行上下文

函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。

Eval 执行上下文:运行在 eval 函数中的代码也获得了自己的执行上下文但由于 JavaScript 开发人员不常用 eval 函数,所以这里不讨论。

执行上下文的生命周期

执行上下文的生命周期包括三个阶段:创建阶段 -> 执行阶段 -> 回收阶段

  1. 创建阶段 当函数被调用,但为执行任何其内部代码之前,会做以下三件事:
  • 创建变量对象:首先初始化函数的参数 arguments ,提升函数声明和变量声明。
  • 创建作用域链(Scope Chain):在执行上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含对象。作用域链用于解析变量。当被要求解析变量时, JavaScript 始终从代码嵌套的最内层开始,如果内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。
  • 确定 this 指向:包括多种情况。 在一段 JS 脚本执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来。变量先暂时赋值为 undefined,函数则先声明好可使用。这一步做完了,然后再开始正式执行程序。

另外,一个函数在执行之前,也会创建一个函数执行上下文环境,跟全局上下文差不多,不过 函数执行上下文中会多出 this arguments 和函数的参数。 2. 执行阶段 执行变量赋值,代码执行 3. 回收阶段 执行上下文出栈等待虚拟机回收执行上下文

执行上下文栈

我们写的函数多了,要如何管理那么多的执行上下文呢?

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

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

ECStack = [];

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

ECStack = [
    globalContext
];

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

// 伪代码

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

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

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

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

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

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

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

实例:

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()();

这两段代码的执行上下文栈的变化不一样: 第一段代码:

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

第二段代码:

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