执⾏上下⽂与执⾏栈

81 阅读2分钟

1. 顺序执⾏

写过 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引擎遇到⼀段怎样的代码时才会做“准备⼯作”呢?

2. 可执⾏代码

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

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

举个例⼦,当执⾏到⼀个函数的时候,就会进⾏准备⼯作,这⾥的“准备⼯作”,让我们⽤个更专业⼀点的说法,就叫做"执⾏上下⽂( execution context )"。

当 JavaScript 代码执⾏⼀段可执⾏代码(executable code)时,会创建对应的执⾏上下⽂(executioncontext)。

对于每个执⾏上下⽂,都有三个重要属性:

  1. 变量对象(Variable object,VO);
  2. 作用域作⽤域链(Scope chain);
  3. this

3. 执⾏上下⽂栈

JavaScript 引擎创建了执⾏上下⽂栈(Execution context stack,ECS)来管理执⾏上下⽂

为了模拟执⾏上下⽂栈的⾏为,让我们定义执⾏上下⽂栈是⼀个数组:

ECStack = [];

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

ECStack = [
    globalContext
];

当JavaScript 遇到下⾯的这段代码了:

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

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

当执⾏⼀个函数的时候,就会创建⼀个执⾏上下⽂,并且压⼊执⾏上下⽂栈,当函数执⾏完毕的时候, 就会将函数的执⾏上下⽂从栈中弹出。知道了这样的⼯作原理,让我们来看看如何处理上⾯这段代码:

// 伪代码

// 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

回顾作用域留下的问题

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

    return f();
}

checkscope();

// case 2
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();

这就是上⽂说到的区别。