• js执行上下文与执行上下文栈


    一、什么是执行上下文

    简单说就是代码运行时的执行环境,必须是在函数调用的时候才会产生,如果不调用就不会产生这个执行上下文。在这个环境中,所有变量会被事先提出来(变量提升),有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文。代码分为三类:全局代码、局部(函数)代码、Eval代码(先不考虑这个),那么也就有三种执行环境,全局执行上下文、函数执行上下文、eval。如图所示:

     全局执行上下文
    * 在执行全局代码前将window确定为全局执行上下文
    * 对全局数据进行预处理
    * var定义的全局变量==>undefined, 添加为window的属性
    * function声明的全局函数==>赋值(fun), 添加为window的方法
    * this==>赋值(window)
    * 开始执行全局代码
    函数执行上下文
    * 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
    * 对局部数据进行预处理
    * 形参变量==>赋值(实参)==>添加为执行上下文的属性
    * arguments==>赋值(实参列表), 添加为执行上下文的属性
    * var定义的局部变量==>undefined, 添加为执行上下文的属性
    * function声明的函数 ==>赋值(fun),
    * this==>赋值(调用函数的对象)
    * 开始执行函数体代码

     二、执行上下文栈

    1.在全局代码执行前, JS引擎就会创建一个来存储管理所有的执行上下文对象,每次调用一个方法A的时候,这个方法可能也会调用另一个方法B,B还可能调用方法C,而JS只能同时一件事,所以
    方法B、C没执行完之前,方法A也不能被释放,那总得找个地方把这些方法按顺序存一存吧,存放的地方就是执行上下文栈,也叫调用栈。
    1. 在全局代码执行前, JS引擎就会创建一个来存储管理所有的执行上下文对象
    2. 在全局执行上下文(window)确定后, 将其添加到栈中(压栈),(最底部)
    3. 在函数执行上下文创建后, 将其添加到栈中(压栈)
    4. 在当前函数执行完后,将栈顶的对象移除(出栈)
    5. 当所有的代码执行完后, 栈中只剩下window

     以下面这个函数举例子:

     画的有点丑

     这个js代码整体有三个执行上下文,window、bar()函数、foo()函数,js代码在第一次加载的时候会先创建一个全局的window执行上下文,如果代码中有函数调用,就创建一个该函数的执行上下文,并将这个上下文推入到执行栈顶,在当前bar函数中又调用了一个函数,继续创建foo函数的执行上下文,并将该上下文推入栈顶,浏览器会一直执行当前栈顶的执行上下文,一旦函数执行完毕,该上下文就会被推出执行栈,直到最后剩一个全局执行上下文。如上图。

    思考:假如bar函数中同时调用了两个函数foo1和foo2,那么这种情况下,栈中会有几个执行上下文?

    依然是只有三个,因为执行foo1函数的时候foo2函数并不会推入栈中,直到foo1函数执行完毕,此时foo1会被推出栈,再加入foo2函数,,所以要牢记,函数执行完就会被弹出栈。

    三、执行上下文的三个阶段

                1.创建阶段

          (1).生成变量对象

          (2).建立作用域链

          (3).确定 this 指向

    在全局执行上下文中,this总是指向全局对象,例如浏览器环境下this指向window对象。

    而在函数执行上下文中,this的值取决于函数的调用方式,如果被一个对象调用,那么this指向这个对象。否则this一般指向全局对象window或者undefined(严格模式)。

        2.执行阶段

          (1).变量赋值

          (2).函数引用

          (3).执行其他代码

     

        3.销毁阶段

          执行完毕出栈,等待回收被销毁

    以下面这个函数作为例子:

    console.log(this)
    
      function fn (a1) {
        console.log(a1)
        console.log(a2)
        a3()
        console.log(this)
        console.log(arguments)
        var a2 = 4
        function  a3 () {
          console.log('a3()')
        }
      }
      fn(2, 3) //调用

    • 首先JS解释器(引擎)开始解释代码,构建执行环境栈(Execution Context Stack),并根据执行环境的不同生成不同的执行上下文(Execution Context)

    • 栈底永远是全局上下文,当遇到fn(),确认调用函数,就创建生成fn自己的上下文,然后将函数执行上下文入栈(push on)到执行环境栈中。

    • 当函数执行完,弹出栈,销毁

    再来看一个例子:

    let a = 20;  
    const b = 30;  
    var c;
    
    function multiply(e, f) {  
     var g = 20;  
     return e * f * g;  
    }
    
    c = multiply(20, 30);

    我们用伪代码来描述上述代码中执行上下文的创建过程:

    //全局执行上下文
    GlobalExectionContext = {
        // this绑定为全局对象
        ThisBinding: <Global Object>,
        // 词法环境
        LexicalEnvironment: {  
            //环境记录
          EnvironmentRecord: {  
            Type: "Object",  // 对象环境记录
            // 标识符绑定在这里 let const创建的变量a b在这
            a: < uninitialized >,  
            b: < uninitialized >,  
            multiply: < func >  
          }
          // 全局环境外部环境引入为null
          outer: <null>  
        },
      
        VariableEnvironment: {  
          EnvironmentRecord: {  
            Type: "Object",  // 对象环境记录
            // 标识符绑定在这里  var创建的c在这
            c: undefined,  
          }
          // 全局环境外部环境引入为null
          outer: <null>  
        }  
      }
    
      // 函数执行上下文
      FunctionExectionContext = {
         //由于函数是默认调用 this绑定同样是全局对象
        ThisBinding: <Global Object>,
        // 词法环境
        LexicalEnvironment: {  
          EnvironmentRecord: {  
            Type: "Declarative",  // 声明性环境记录
            // 标识符绑定在这里  arguments对象在这
            Arguments: {0: 20, 1: 30, length: 2},  
          },  
          // 外部环境引入记录为</Global>
          outer: <GlobalEnvironment>  
        },
      
        VariableEnvironment: {  
          EnvironmentRecord: {  
            Type: "Declarative",  // 声明性环境记录
            // 标识符绑定在这里  var创建的g在这
            g: undefined  
          },  
          // 外部环境引入记录为</Global>
          outer: <GlobalEnvironment>  
        }  
      }

    在执行上下文创建阶段,函数声明与var声明的变量在创建阶段已经被赋予了一个值,var声明被设置为了undefined,函数被设置为了自身函数,而let  const被设置为未初始化。

    现在你总知道变量提升与函数提升是怎么回事了吧,以及为什么let const为什么有暂时性死域,这是因为作用域创建阶段JS引擎对两者初始化赋值不同。

    上下文除了创建阶段外,还有执行阶段,这点大家应该好理解,代码执行时根据之前的环境记录对应赋值,比如早期var在创建阶段为undefined,如果有值就对应赋值,像let const值为未初始化,如果有值就赋值,无值则赋予undefined。

     五、练习

     想想这段代码的输出顺序是什么

    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)
    

    这是一个递归调用,在不满足条件的情况下函数会一直调用自己,直到条件满足函数就结束调用,继续上图:

     很多人都觉得fe输出3就结束了,怎么还会有2和1,其实return只是结束了最后一次调用,没有执行完的代码会按出栈顺序执行完。

    所以依次输出为:

    gb: undefined
    fb: 1
    fb: 2
    fb: 3
    fe: 3
    fe: 2
    fe: 1
    ge: 1
    不积跬步无以至千里
  • 相关阅读:
    重建二叉树
    字符串移位包含的问题
    整数的逆序存储
    容器的综合应用:文本查询程序(摘自C++ Primer)
    vsprintf 变参函数可以用
    常用项目依赖(前端)
    eslint一些常见配置
    Jscrpit中的原型对象
    html网页自适应手机屏幕大小
    A Bit of Fun
  • 原文地址:https://www.cnblogs.com/lyt0207/p/12034893.html
Copyright © 2020-2023  润新知