阅读 408

深入学习js之——执行上下文栈#3

深入学习js系列是自己阶段性成长的见证,希望通过文章的形式更加严谨、客观地梳理js的相关知识,也希望能够帮助更多的前端开发的朋友解决问题,期待我们的共同进步。

如果觉得本系列不错,欢迎点赞、评论、转发,您的支持就是我坚持的最大动力。


开篇

作为一个JavaScript的程序开发者,如果被问到JavaScript代码的执行顺序,你脑海中是不是有一个直观的印象 -- 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中的变量提升比如函数提升都是在这个准备阶段完成的。

本文我们就来深入的研究一下,这一段一段中的是如何划分的呢?

到底JavaScript引擎遇到一段怎样的代码才会做"准备工作"呢?为了解答这个问题我们引入一个概念——执行上下文

执行上下文

如果你做过小学的阅读理解,肯定见到过这样的题目:联系上下文解释句子,这里的上下文指的可能是这个句子所在的段落,也可能是这个句子所在段落的临近段落。实际上,这里描述的是一个句子的语境和作用范围,联系类比到程序中我们可以作如下定义:

执行上下文是当前JavaScript代码被解析和执行时所在环境的抽象概念。

执行上下文的类型

执行上下文总共分为三种类型,有时候我们也叫做可执行代码(executable code)

  • 全局执行上下文: 只有一个,浏览器中的全局对象就是window对象,this指向这个全局对象。
  • 函数执行上下文: 存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。
  • Eval 函数执行上下文: 指的是运行在eval函数中的代码,很少用而且不建议使用。

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

执行栈

接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢?所以 JavaScript 引擎创建了执行上下文栈(Execution context stack )ECStack 来管理执行上下文。

这里我们可以简单的认为 ECStack 是一个数组,类似这样:

ECStack = [];
复制代码

执行栈,也叫做调用栈,具有 LIFO(last in first out 后进先出) 结构,用于存储在代码执行期间创建的所有执行上下文。

  • 首次运行JavaScript代码的时候,会创建一个全局执行的上下文并Push到当前的执行栈中,每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push当前执行栈的栈顶。
  • 当栈顶的函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文的控制权将移动到当前执行栈的下一个执行上下文。

让我们看一段代码来理解这个过程:

var a = 'Hello World!';

function first() {  
  console.log('Inside first function');  
  second();  
  console.log('Again inside first function');  
}

function second() {  
  console.log('Inside second function');  
}

first();  
console.log('Inside Global Execution Context');

// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context
复制代码
  • 当上述代码在浏览器加载时,JavaScript引擎创建了一个全局执行上下文并把它压入(push) 当前的执行栈。当遇到 first() 函数调用时,JavaScript引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部
  • 当从 first() 函数内部调用 second() 函数时,JavaScript引擎为 second() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部,当 second() 函数执行完毕,它的执行上下文会从当前栈弹出(pop),并且控制流程到达下一个执行上下文,即 first() 函数的执行上下文。
  • first() 执行完毕,它的执行上下文从栈中弹出,控制流程到达了全局执行上下文。一旦所有的代码执行完毕,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()();
复制代码

两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?

答案就是执行上下文栈的变化不一样。

让我们模拟第一段代码:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
复制代码

让我们模拟第二段代码:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
复制代码

checkscope()();这里对于这个函数的执行做一些解释

// checkscope()() 就相当于
var f = checkscope();
f();
复制代码

checkscope 函数执行,函数执行完毕后,该函数返回一个函数名,就相当于:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
复制代码

然后再执行的这个返回的函数,就相当于:

ECStack.push(<f> functionContext);
ECStack.pop();
复制代码

为了更详细讲解两个函数执行上的区别,我们需要探究一下执行上下文到底包含了哪些内容,我们需要更加深入了解变量对象的相关内容。

参考链接:

《理解 JavaScript 中的执行上下文和执行栈》

《JavaScript深入之执行上下文栈》

深入学习JavaScript系列目录

欢迎添加我的个人微信讨论技术和个体成长。

欢迎关注我的个人微信公众号——指尖的宇宙,更多优质思考干货