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
不积跬步无以至千里
原文地址:https://www.cnblogs.com/lyt0207/p/12034893.html