js执行上下文

309 阅读13分钟
/**
前言
总结的ES3 ES6的描述
什么是执行上下文
js代码执行的环境
  执行上下文的类型
  *全局执行上下文:这个是默认的或者说基础的上下文,任何不在函数内部的代码都在全局上下文中,它会执行两件事,创建一个全局的window对象(浏览器情况下),并且设置this的值等于这个全局对象,一个程序只会有一个全局执行上下文
  *函数执行上下文:每当一个函数被调用时,都会为该函数创建的一个新的上下文,不过是在函数被调用时创建的,函数上下文可以有任意多个,每当一个新的执行上下文被创建,他会按定义的顺序执行一系列步骤
  *Eval函数执行上下文;执行在eval函数内部的代码也会有它属于自己的执行上下文,但是由于JavaScript开发者不用所以不讨论

ES3执行上下文的内容
  执行上下文是一个抽象的概念,我们可以理解为一个object,一个执行上下文里包括以下内容
  1. 变量对象VO variable object
    每个执行环境文都有一个表示变量的对象-变量对象,全局执行环境的变量对象始终存在,而函数这样的局部环境变量,只会在函数执行的过程中存在,在函数被调用时,并且在具体的函数代码运行之前,JS引擎会用当前函数的参数列表(arguments)初始化一个变量对象,并将当前执行上下文与之关联,函数代码块中声明的变量和函数将作为属性添加到这个变量对象上
    【有需要注意,只有函数声明(function  declaration)会被加入到变量对象中,而函数表达式(function expression)会被忽略】
    // 这种叫函数声明,会被加入变量对象
    function demo() {}
    // tmp 是变量声明也会加入到变量对象 但是作为一个函数表达式 demo2 不会加入变量对象
    var tmp = function demo2() {}

    全局执行上下文和函数执行上下文的变量和对象还略有不同,之间的差别简单来说
    *1. 全局上下文中的变量对象就是全局对象,以浏览器环境来说,就是window对象
    *2. 函数执行上下文中的变量对象内部定义的属性 是不能直接被访问的,只有当函数被调用时候,变量对象(VO)被激活为活动对象(AO)时,我们才能访问到其中的属性和方法

  2. 活动对象AO activation object
    函数进入执行阶段时候,原本不能访问的变量对象被激活成为一个活动对象,自此我们可以访问到其中的各种属性
    【其实变量对象和活动对象是一个东西只不过处在不同的状态和阶段】

  3. 作用域链
    作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,会从父级(词法层面的父级),执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局变量,这样有多个执行变量构成的链表叫做作用域链

  4. 调用者信息this 当前可执行代码块的调用者
    如果当前函数被作为对象方法调用或者使用bind call apply 等API进行委托调用,则将当前代码块的调用者信息(this.value)存入当前执行上下文否则默认为空全局对象调用

  5. 执行上下文数据结构模拟
    如果将上述完整的执行上下文使用代码形式表现出来的话,应该类似如下
    executionContext:{
      [variable object | activation object]: {
        arguments,
        variables: [...],
        functions: [...],
      },
      scope chain: variable object + all parents scope
      thisValue: contentx obeject
    }

ES3执行上下文生命周期
  1. 创建阶段
  函数执行上下文的创建阶段,发生在函数调用时并且在执行函数体内的具体代码之前,在创建阶段,js引擎会做如下操作  
    1.1 全局执行上下文
    *执行全局代码前创建全局执行上下文
    *对全局数据进行预处理
      *这个阶段会进行 【变量提升和函数声明】
      *var 定义的全局变量 -> undefined添加为window属性
      *function 声明的全局函数 -> 赋值(fun)添加为window属性
      *this -> 赋值(window)

    1.2 函数执行上下文
    *在调用函数时,准备执行函数体之前,创建对应的函数执行上下文对象
    *对局部数据进行预处理
      *形参变量 -》赋值(实参)-》添加为执行上下文的属性
      *arguments-》赋值-》(实参列表),添加为执行上下文属性
      *var 定义的局部变量-》undefined添加为执行上下文属性
      *function声明的函数-》赋值(func)添加为执行上下文属性
      *构建作用域链
      *this-》赋值(调用函数对象)
      【有没有发现这个创建执行上下文的阶段有变量和函数的初始化生命。这个操作就是 **变量声明提升**(变量和函数声明都会提升,但是函数提升更靠前)】
  2. 执行阶段
    执行阶段,js代码逐条执行,js引擎开始对定义的变量赋值,开始顺着作用域链访问变量,如果内部有函数调动就创建一个新的执行上下文,压入执行栈 并把控制权交出
  3. 销毁阶段
  般来讲当函数执行完成后,当前执行上下文(局部环境)会被弹出执行上下文栈并且销毁,控制权被重新交给执行栈上一层的执行上下文。
  【注意这只是一般情况,闭包的情况又有所不同。闭包的定义:有权访问另一个函数内部变量的函数,简单来说如果一个函数被作为另一个函数的返回值并在外部被引用,那么这个函数被称为闭包】
ES3执行上下文总结
  1. 函数被调用
  2. 在执行具体的函数代码之前,创建了执行上下文
  3. 进入执行上下文的创建阶段:
    1. 初始化作用域链
    2. 创建 arguments object 检查上下文中的参数,初始化名称和值并创建引用副本
    3.扫描上下文找到所有函数声明:
      对于每个找到的函数,用它们的原生函数名,在变量对象中创建一个属性,该属性里存放的是一个指向实际内存地址的指针
      如果函数名称已经存在了,属性的引用指针将会被覆盖

    4. 扫描上下文找到所有var的变量声明:
      对于每个找到的变量声明,用它们的原生变量名,在变量对象中创建一个属性,并且使用 undefined 来初始化
      如果变量名作为属性在变量对象中已存在,则不做任何处理并接着扫描
    5. 确定 this 值
  4. 进入执行上下文的执行阶段
    在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。

ES5执行上下文
  ES5 规范对ES3中的执行上下文部分概念做了调整,最重要的是去除了ES3中的变量对象和活动对象,以词法环境组件(LexicalEnvironment component)和变量环境组件(VariableEnviroment component)替代,所以ES5 的执行上下文概念上表示大概如下
  ExecutionContext = {
    This.Binding = <this value>,
    LexicalEnvironment = {...},
    VariableEnvironment = {...}
  }

  This.Binding
    * 全局执行上下文中,this 指向全局对象,在浏览器中this的值指向window对象,而在nodejs中指向这个文件的module对象
    * 函数执行上下文中,this 的值取决于函数的调用方式,具体有默认绑定,隐式绑定 显示绑定(硬绑定) new绑定 箭头函数 具体内容会在【this全面解析】*******??????部分详解。
    * 
  词法环境
    词法环境有两个组成部分
    1. 环境记录: 存储变量和函数声明的实际位置
    2. 对外部环境的引用: 可以访问其外部词法环境

    词法环境有两种类型
      1. 全局环境: 是一个没有外部环境的词法环境,其外部环境引用为null,拥有一个全局对象(window对象)及其关联的方法和属性,例如数组方法,已经任何用户自定义的全局变量, this 的值指向这个全局对象
      2. 函数环境: 用户在函数中定义的变量被村粗在 环境记录中,包含了arguments 对象,对外部环境的引用可以是全局环境,也可以是内部函数的外部函数环境

      直接按伪代码可能更加直观
      GlobalExectionContext = { // 全局执行上下文
        LexicalEnvironment: { // 词法环境
          EnvironmentRecord: { // 环境记录
            Type: "Object",  // 全局环境
            标识符绑定在这里
            outer: <null> //  对外部环境的引用
          }
        }
      }
      Functio你ExectionContext = { // 函数执行上下文
        LexicalEnvironment: { // 词法环境
          EnvironmentRecord: { // 环境记录
            Type: "Declarative", // 函数环境
            // 标识符绑定在这里 // 对外部环境的引用
            outer: <Global or outer function enviroment reference>
          }
        }
      }

  变量环境
      变量环境也是一个词法作用域,因此它具有上面定义的词法环境和所有属性
      在ES6中,词法环境 和 变量环境 的区别在于前者用于存储,函数声明和变量(let 和constructor) 绑定,而后者仅用于村粗变量(var) 绑定
      下面例子
      let a = 20
      const b = 30
      var c
      function multiplt (e, f) {
        var g = 20
        return e * f *g
      }
      c = multiplt(20, 30)
      执行上下文如下所示
      GlobalExectionContext = {
        ThisBinding: <Global Object>,
        lexiacalEnvironment: {
          EnvironmentRecord: {
            Type: "Object",
            a: <uninitialized>,
            b: <uninitialized>,
            multiply: <func>
          }
          outer: <null>
        },
        VariableEnvironment: {  
          EnvironmentRecord: {  
            Type: "Object",  
            // 标识符绑定在这里  
            c: undefined,  
          }  
          outer: <null>  
        }
      }

      FunctionExectionContext = {
        ThisBinding: <Global Object>,
        LexicalEnvironment: {  
          EnvironmentRecord: {  
            Type: "Declarative",  
            // 标识符绑定在这里  
            Arguments: {0: 20, 1: 30, length: 2},  
          },  
          outer: <GlobalLexicalEnvironment>  
        },
        VariableEnvironment: {  
          EnvironmentRecord: {  
            Type: "Declarative",  
            // 标识符绑定在这里  
            g: undefined  
          },  
          outer: <GlobalLexicalEnvironment>  
        }  
      }
      变量提升的原因:在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 letconst 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问
      letconst 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。

ES5执行上下文总结
  对于ES5中的执行上下文,我们可以用这个列表来概括程序执行的整个过程
  1. 程序启动 全局上下文被创建
    1. 创建全局上下文的词法环境
      1. 创建 对象环境记录器,它用来定义出现在全局上下文中的变量和函数的关系(负责处理letconst 定义的变量)
      2. 创建 外部环境引用,值为null
    2. 创建全局上下文的变量环境
        1. 创建对象环境记录器,它持有 【变量声明语句】在执行上下文中创建的绑定关系(负责处理 var定义的变量,初始值为undefined造成声明提升)
        2. 创建 外部环境引用 值为null
    3. 确定this值为全局对象,以浏览器为例子 就是window
  2. 函数被调用,函数上下文被创建
    1. 创建函数上下文的词法环境
      1. 创建 声明环境记录器,存储变量、函数和参数、它包含了一个传递给函数的arguments对象(此对象,存储索引和参数的映射)和传递给函数的参数的length(负责处理letconst 定义的变量)
      2. 创建 外部环境引用,值为全局对象,或者为父级词法作用域
    2. 创建函数上下文的变量环境
      1. 创建声明式环境记录器,存储变量、函数和参数, 它包含了一个传递给函数的arguments对象(此对象存储索引和参数的映射)和传递给参数的length(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
      2. 创建外部环境引用,值为全局对象,或者为父级词法作用域
    3. 确定this值
  3. 进入函数执行上下文的执行阶段
    1. 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值

执行栈
执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文
let 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');
上述代码的执行上下文栈。
当上述代码在浏览器加载时,JavaScript 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到 first() 函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。
当从 first() 函数内部调用 second() 函数时,JavaScript 引擎为 second() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 second() 函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 first() 函数的执行上下文。
当 first() 执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

结论
1. 执行上下文创建阶段分为绑定this,创建词法环境,变量环境三步,两者区别在于词法环境存放函数声明与const let声明的变量,而变量环境只存储var声明的变量。
2. 词法环境主要由环境记录与外部环境引入记录两个部分组成,全局上下文与函数上下文的外部环境引入记录不一样,全局为null,函数为全局环境或者其它函数环境。环境记录也不一样,全局叫对象环境记录,函数叫声明性环境记录。
3. 你应该明白为什么会存在变量提升,函数提升,而let const没有。
4. 
 */