执行上下文和执行栈

波斯猫:没有那种世俗的欲望

执行上下文是一个抽象的概念,JavaScript引擎在执行代码之前会进行一些“准备工作”,这个“准备工作”就是执行上下文。可以认为所有的JavaScript代码都是运行在执行上下文中的。

1.变量和函数提升

首先来看下面的代码。

var a = 3
function fun() {
    console.log(a)
    var a = 4
}
fun()//undefined 在函数内部var将变量声明提升却未赋值,函数优先访问的是内部的变量
fun2()//fun2 函数声明提升,让函数可以在其定义之前的地方被调用
fun3() //无法调用,fun3虽然是函数但是是根据函数表达式定义的,会按照变量提升处理

function fun2() {
    console.log("fun2")
}

var fun3 = function () {
    console.log("fun3")
}

代码中提出了两个主要的概念,变量提升和函数提升。使用var来声明的变量,在后续代码执行前就会被声明,并且赋值为undefined。而使用函数声明来创建的函数,也会在后续代码执行前就被定义好。这就是执行上下文的作用之一。
值得注意的是,变量提升是先于函数提升进行的。同时命名一个同名的函数和变量就可以检验这一点。

  function a() {}
  var a
  console.log(typeof a) // 'function'

2.执行上下文

执行上下文是代码正式执行前的准备工作,那么这个准备工作具体是什么呢?根据执行上下文的类型准备工作也有所区别,执行上下文主要分为全局执行上下文和函数执行上下文。

全局执行上下文

  1. 创建全局执行上下文对象window
  2. 全局数据预处理
  • 找到var声明的变量,赋值为undefined,添加为window的属性。
  • 找到函数声明方式定义的函数,赋值并添加为window的方法。
  • 将this指向window

函数指向上下文

  1. 创建函数指向上下文对象
  2. 对局部数据预处理
  • 找到形参变量,将实参的值赋给形参,添加为函数执行上下文的属性。
  • 将arguments赋值(arguments为实参的列表,是一个伪数组),添加为函数执行上下文的属性。
  • 找到var声明的变量,赋值为undefined,添加为函数执行上下文的属性。
  • 找到函数声明方式定义的函数,赋值并添加为函数执行上下文的方法。
  • 将this指向window

3.执行上下文栈

栈是一种基本的数据结构,具有后进先出(LIFO)的特性。JS引擎在遇到JS代码的时候,就会创建一个全局执行上下文,然后将它压入栈。执行上下文栈栈底的始终是全局执行上下文,因为在执行JS代码前它就被压入栈了。之后,JS引擎依次执行代码,在遇到一个函数的调用的时候,JS引擎会创建一个函数执行上下文并将它压入栈。函数执行上下文只有在函数被调用的时候才会被创建,函数只定义不调用并不会生成执行上下文。
按照上面的顺序,上下文对象被压入栈,JS引擎只会执行那些位于栈顶的上下文中的代码。一个上下文对象中的代码被执行完,它就会被弹出栈,栈顶元素发生改变,JS继续执行栈顶的上下文对象中的代码直到栈顶元素变成全局上下文。全局上下文对象只有在程序关闭的时候才会被弹出栈。
首先来看一个简单的代码。

    /*1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
      2. 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
      3. 在函数执行上下文创建后, 将其添加到栈中(压栈)
      4. 在当前函数执行完后,将栈顶的对象移除(出栈)
      5. 当所有的代码执行完后, 栈中只剩下window*/

    //1.执行代码前,将window作为全局执行上下文压入栈
    var a = 10
    var bar = function (x) {
        var b = 5
        foo(x + b)//3.创建另一个函数执行上下文对象,压栈
    }
    var foo = function (y) {
        var c = 5
        console.log(a + c + y)
    }
    bar(10)//2.调用函数,创建函数执行上下文对象,压栈
    //序号代表执行上下文创建和执行的顺序

    //上述代码一共创建了三个执行上下文对象

然后是一个稍微复杂一些的例子,关于递归,试着判断一下gb fb fe ge的值。

    console.log('gb: '+ i)
    var i = 1
    foo(1)
    function foo(i) {
        if (i == 4) {
            return
        }
        console.log('fb:' + i)
        foo(i + 1) //递归调用: 在函数内部调用自己
        console.log('fe:' + i)
    }
    console.log('ge: ' + i)

满足终止条件之前,函数不断地调用自身,也就是在不断地创建执行上下文压入栈。前一次地执行上下文在下一次地执行上下文创建地时候依旧存在于执行栈中。一张图片可以简单地解释这段代码。


4.结论

执行上下文决定了JavaScript代码的执行顺序,JS引擎总是执行那些位于执行栈顶的执行上下文中的代码,代码被执行完毕后,执行上下文就会被弹出栈,内部的变量也会被释放。
同时需要记住,执行上下文是动态的,只有在执行JS代码、调用函数的时候才会生成执行上下文,函数只定义而不调用是不会生成执行上下文的。执行上下文被执行完后会被弹出栈。所以称执行上下文是动态的。后续要区分它和作用域的区别。

推荐阅读文章

[译] 理解 JavaScript 中的执行上下文和执行栈 - 掘金 (juejin.cn)
JavaScript深入之执行上下文栈 - 掘金 (juejin.cn)

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容