简单介绍的执行上下文和执行栈

什么是执行上下文?

执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。Javascript 代码都是在执行上下文中运行。javascript

JavaScript 的可执行代码(executable code)的类型只有三种,全局代码、函数代码、eval代码。前端

对应着,JavaScript 中有三种执行上下文类型。java

  • 全局执行上下文 — 默认的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:建立一个全局的 window 对象(浏览器的状况下),而且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数建立一个新的上下文。每一个函数都有它本身的执行上下文,函数上下文能够有任意多个。
  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于本身的执行上下文

举个栗子,当执行到一个函数的时候,就会进行准备工做,这里的“准备工做”,就是准备"执行上下文(execution context)"。git

执行栈

执行栈,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时建立的全部执行上下文。github

当 JavaScript 开始要解释执行代码的时候,它会建立一个全局的执行上下文而且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数建立一个新的执行上下文并压入栈的顶部。面试

程序结束以前, 执行栈最底部永远有个全局上下文浏览器

引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。数据结构

模拟js执行如下代码:函数

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

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();
复制代码

定义执行上下文栈:ECStack = [];post

  1. 向栈中压入全局上下文:ECStack.push(globalContext);
  2. 执行fun1,建立fun1上下文,并压入执行栈:ECStack.push(fun1Context);
  3. 执行fun2,建立fun2上下文,并压入执行栈:ECStack.push(fun2Context);
  4. 执行fun3,建立fun3上下文,并压入执行栈:ECStack.push(fun3Context);
  5. fun3执行完毕,弹出并销毁fun3上下文:ECStack.pop();
  6. fun2执行完毕,弹出并销毁fun2上下文:ECStack.pop();
  7. fun1执行完毕,弹出并销毁fun1上下文:ECStack.pop();
  8. 全部代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

怎么建立执行上下文?

建立执行上下文有两个阶段:1) 建立阶段2) 执行阶段

在建立阶段会发生三件事:

  1. This 绑定
  2. 建立词法环境组件。
  3. 建立变量环境组件。

或者你也能够简单理解为:

  1. 函数上下文环境参数的绑定(arguments)
  2. 函数表达式提高(hoist)
  3. 变量的声明,并将var声明的变量初始值设置为 undefined (hoist)

因此执行上下文在概念上表示以下:

ExecutionContext = {
  ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, } 复制代码

在函数执行上下文中,this 的值取决于该函数是如何被调用的。若是它被一个引用对象调用,那么 this 会被设置成那个对象,不然 this 的值被设置为全局对象或者 undefined(严格模式下)

词法环境对象

词法环境和变量环境组件始终为 词法环境对象。

变量环境也是一个词法环境,它有着词法环境的全部属性。

在 ES6 中,词法环境组件和变量环境的一个不一样就是前者被用来和变量(letconst)绑定,然后者用来存储函数声明和 var 变量绑定。即:

  • let、const声明的变量,外部环境引用保存在词法环境组件中。
  • var和function声明的变量和保存在环境变量组件中。

每一个词法环境对象包含两部分:

  • 环境记录器
  • 外部环境的引用(可能为空,好比全局词法环境就没有外部引用)

如下面代码为例:

let a = 1;
const b = 2;
var c = 3;
function test (d, e) {
  var f = 10;
  return f * d * e;
}
c = test(a, b);
复制代码

解析阶段的全局环境内的词法环境和变量环境

GlobalLexicalEnvironment = {
  LexicalEnvironment: { // 词法环境组件
    OuterReference: null, // 全局词法环境中外部引用为空
    EnviromentRecord: {
      Type: 'object',
      a: <uninitialized> , // let 和 const 变量绑定但未关联值
      b: <uninitialized> 
    },
  },
  VariableEnvironment: { //变量环境组件
    EnviromentRecord: {
      type: 'object',
      test: <func>,
      c: undefined,  // var变量会被初始为 undefined
    }
  }
}
复制代码

解析test时的词法环境和变量环境

注意:只有调用函数时,函数执行上下文才会被建立

// 此时 全局上下文已经执行,所以 a、b、c都已经与对应值关联
GlobalLexicalEnvironment = {
  LexicalEnvironment: {
    OuterReference: null,
    EnviromentRecord: {
      Type: 'object',
      a: 1 ,
      b: 2 
    },
  },
  VariableEnvironment: {
    EnviromentRecord: {
      type: 'object',
      c: 3,,
      test: <func>
    }
  }
}

// test的词法执行上下文开始构建,var变量绑定但未赋值,形参绑定
FunctionLexicalEnvironment = {
  LexicalEnvironment: {
    OuterReference:  <GlobalLexicalEnvironment>,
    EnviromentRecord: {
      Type: 'Declarative',
      arguments: {0: 1, 1: 2, length: 2}
    },
  },
  VariableEnvironment: {
    EnviromentRecord: {
      Type: 'Declarative',
      f: undefined,
    }
  }
}
复制代码

插播一条变量提高的知识点:

在建立执行上下文时,js引擎会检查当前做用域的全部变量声明及函数声明,在执行以前,var声明的变量已经绑定初始undefined,而在let和const只绑定在了执行上下文中,但并未初始任何值,因此在声明以前调用则会抛出引用错误(即TDZ暂时性死区),这也就是函数声明与var声明在执行上下文中的提高。

let/const也存在变量提高现象,详情移至你可能不知道的变量提高

执行阶段

在执行上下文的建立阶段,完成了变量声明,在代码的执行阶段,才会完成对变量真正的赋值。

在执行阶段,若是 JavaScript 引擎不能在源码中声明变量的实际位置找到 let 变量的值,它会被赋值为 undefined

最后,看一个《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()();
复制代码

两段代码执行的结果同样,都是local scope,若不理解,请移步词法做用域及做用域链讲解

可是两段代码究竟有哪些不一样呢?

模拟第一段代码:

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

模拟第二段代码:

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

ps: 这篇文章写的很困难,搜集资料的时候被各类词语及讲解弄的很懵,有些逻辑还有冲突,考虑了好久才决定只写这些内容,将这篇文章只做为对执行上下文的简单描述而不是详细讲解,由于再写多了,一些概念会使文章很难被阅读和理解,等后续我有了深刻的理解再更新内容吧。若是有错误之处,欢迎在评论中指出~

相关系列: 从零开始的前端筑基之旅(面试必备,持续更新~)

若是你收获了新知识,请给做者点个赞吧,让更多的人看到它~

参考文章:

  1. ****JavaScript深刻之执行上下文栈****
  2. ****[译] 理解 JavaScript 中的执行上下文和执行栈****
  3. ****也来谈谈JS的执行上下文与词法环境****
相关文章
相关标签/搜索