深入浅出执行上下文、词法环境、变量环境

1,528 阅读6分钟

执行上下文的概念

执行上下文:javascript 代码解析和执行时所在的环境。

执行上下文的类型

执行上下文分为三种类型:

1.全局执行上下文

  • js代码开始运行后。首先进入全局执行上下文环境中,不在任何函数中的js代码都会在全局执行上下文中
  • 一个js程序中只存在一个全局执行上下文。创建时会压人栈底,只有当程序结束时才会弹出
  • 全局执行上下文会做两件事。1.创建全局对象,2.将this指向这个全局对象
  • 浏览器环境中全局对象是window, 在node环境中全局对象是global

2.函数执行上下文

  • 函数每次调用都会产生一个新的函数执行上下文,每个函数都拥有自己的执行上下文,但是只有调用的时候才会被创建
  • 函数执行上下文的生命周期分为两个阶段。创建和执行
  • 每一个执行上下文中都有以下三个属性
    • VO变量对象
    • 作用域链 scope chain
    • this

3.Eval执行上下文

  • eval函数执行时产生的执行上下文。

执行上下文栈

执行上下文栈是一个后进先出的数据结构, 具体执行流程如下

  • 首先创建全局执行上下文, 压入栈底
  • 每当调用一个函数时,创建函数的函数执行上下文。并且压入栈顶
  • 当函数执行完成后,会从执行上下文栈中弹出,js引擎继续执栈顶的函数。

如以下函数执行时的执行栈变化为:

function fun1(){
    console.log('func1')
    fun2()
}
function fun2(){
    console.log('func2')
}
fun1()	
/*
*		      fun2
*	    fun1      fun1	fun1	
* global => global => global => global => global
*/

执行上下文生命周期

变量对象VO和活动对象AO

在讲生命周期前。我们必须了解讲个对象,变量对象VO和活动对象AO

变量对象VO:

  • 用来存储执行上下文中可以被访问。但不能被delete的函数标识。
  • 包括:arguments 对象,形参实参键值对,函数声明,变量声明和this 活动对象AO:
  • 他可以被理解为当函数被激活调用时创建的对象。用来访问VO对象中存储的那些个标识。 全局上下文中变量对象:
  • 就是全局对象
  • 初始化时:初始化一系列原始属性:Math,String,Date,Window等
  • 在浏览器中window对象引用全局对象,全局环境中this也引用自身

创建阶段

此阶段执行上下文会执行以下操作

  • 创建作用域链
  • 通过变量对象VO创建活动AO,
    • 首先创建arguments对象
    • 创建形参实参的键值对
    • 创建函数声明 (经典面试题:为什么函数声明提前?)
    • 创建变量声明 (经典面试题:为什么变量声明提升?)
  • 创建this

执行阶段

  • 变量赋值。函数引用,执行其他代码逻辑
  • 当执行完毕后。执行上下文出栈,等待垃圾回收机制回收

举例说明

function a(name, age){
    var a = 1
    function c(){}
    var d = funciton (){}
    (function e(){})
    var f = function g(){}
}
a('John')
// 如上代码。
//在创建预编译阶段生成的AO对象如下
AO = {
    arguments:{
    	0: 'John',
        1: undefined,
        length: 2
    },
    name: 'John',
    age: undefined,
    c: reference to function c(){},
    a: undefined,
    d: undefined,
    f: undefined,
}
// 函数表达式 e,不在AO中 
// 函数g不在AO中

// 函数执行时AO对象如下
AO = {
    arguments:{
    	0: 'John',
        1: undefined,
        length: 2
    },
    name: 'John',
    age: undefined,
    c: reference to function c(){},
    a: 1,
    d: reference to FunctionExpression "d",
    f: reference to FunctionExpression "f",
}

函数声明提前

从AO对象的创建过程我们就可以发现,AO对象是先扫描函数体内的函数声明才去扫描变量声明。所以这也就是为啥会有声明提前。

变量提升

AO对象创建时已经将函数内部的变量提前扫描声明。是指在函数执行的过程中开始依次赋值。

词法环境和变量环境

在ES6中提出词法环境和变量环境两个概念。主要是执行上下文创建过程。

词法环境(LexicalEnvironment)

词法环境是一种包含 标识符 => 变量 隐射关系的一种结构。

在词法环境中有两个组成部分:

  • 环境记录(EnvironmentRecord): 储存变量和函数声明的实际位置
  • 对外部环境的引用(Outer):当前可以访问的外部词法环境

词法环境分为两种类型:

  • 全局环境: 全局执行上下文,他没有外部环境的引用,拥有一个全局对象window和关联的方法和属性eg: Math,String,Date等。还有用户定义的全局变量,并将this指向全局对象。
  • 函数环境: 用户在函数定义的变量将储存在环境记录中。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。环境记录中包含。用户声明的变量。函数。还有arguments对象。

举例词法环境在伪代码中如下:

GlobalExectionContent = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 剩余标识符
    },
    Outer: null,
  }
}

FunctionExectionContent = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 剩余标识符
    },
    Outer: [Global or outer function environment reference],
  }
}

变量环境(VariableEnvironment)

变量环境也是一个词法环境。他具有词法环境中所有的属性 在ES6中,LexicalEnvironment和VariableEnvironment 的区别在于前者用于存储函数声明和变量let 和 const 绑定,而后者仅用于存储变量 var 绑定。

用以下代码举例:

let a = 20;  
const b = 30;  
var c;

function add(e, f) {  
 var g = 20;  
 function c(){}
 return e + f + g;  
}

c = add(20, 30);

在预编译阶段。生成的词法环境和变量环境如下

GlobalExectionContent = {
  thisBinding: Global,
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      a: <uninitialied>,
      b: <uninitialied>,
      add: <func>
      // 剩余标识符
    },
    Outer: null,
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      c: undefined,
      // 剩余标识符
    },
    Outer: null,
  }
}

FunctionExectionContent = {
  thisBinding: Global,
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      arguments: {
        0: 20,
        1: 30,
        length: 2,
      },
      e: 20,
      f: 30,
      c: reference to function c(){}
      // 剩余标识符
    },
    Outer: GlobalLexicalEnvironment,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      g: undefined,
      // 剩余标识符
    },
    Outer: GlobalLexicalEnvironment,
  }
}

我们发现使用let和const声明的变量在词法环境创建时是未赋值初始值。而使用var定义的变量在变量环境创建时赋值为undefined。这也就是为什么const、let声明的变量在声明钱调用会报错,而var声明的变量不会。

类比我们的执行上下文来说就是这样

代码执行阶段

此阶段的执行流程就是函数执行时的流程。给变量赋值,和执行其他逻辑代码。

关注公众号

喜欢的同学可以关注公众号