ES6:理解词法环境和变量环境

2,525 阅读6分钟

一、词法环境

image.png 1. 词法环境是用来定义 基于词法嵌套结构的ECMAScript代码内的标识符与变量值和函数值之间的关联关系的一种规范类型。
2. 一个词法环境由环境记录(Environment Record)和一个可能为null的对外部词法环境的引用(Outer Lexical Environment)组成。
3. 一个词法环境经常用于关联一个函数声明、代码块语句、try-catch语句,当它们的代码被执行时,词法环境被创建出来 简单来说,词法环境就是标识符与它们的值之间的映射关系,类似于作用域链

词法环境有两个主要组成部分: 环境记录(Environment Record) 、外部词法环境引用(Outer Lexical Environment)

(一)环境记录(Environment Record)

image.png 它有两种主要的环境记录值: 声明式环境记录(Declarative Environment Record)、对象式环境记录(Object Environment Record),和一种特殊的环境记录值: 全局环境记录(Global Environment Record)`

可以把环境记录(Environment Record)抽象为特殊的对象,可以当作ES3中的VO(变量对象)和AO(活动对象)

当前词法环境中所有的标识符都会在环境记录中,任何在环境记录中的标识符都可以在当前词法环境直接以标识符形式访问

1. 对象式环境记录(Object Environment Record)

  • 它有一个关联的绑定对象(binding object),记录全局var声明、函数声明等与某些对象属性相绑定的标识符

  • 将对象式环境记录中所有的标识符绑定到绑定对象(binding object)的属性上,标识符与绑定对象(binding object)的属性名一一对应

    例如: var age = 100 , window.age = 100

  • 每个标识符在绑定后都会直接初始化为undefined(故var声明有变量提升) ,如果标识符已经绑定了binding object的原有属性,那么该变量就是对应属性值,标识符可以重复声明

    例如: var age =100 , var age , age = 100

2. 声明式环境记录(Declarative Environment Record)

  1. 每个声明式环境记录项都和ECMAScript程序方位内的(除了var以外)的变量、常量、let、const、class、module、import或者函数声明相关联。一个声明式环境记录项绑定了一序列在其范围内的声明的标识符。

  2. 将所有非var声明的标识符实例化,但不初始化,也就是变量处于uninitialized状态(所以let 和const 有暂时性死区),也就是说内存中已经为变量预留出空间,但是还没有和对应的标识符建立绑定关系。但是JS引擎对函数的声明进行了特殊的处理,允许像var那样进行提升

  3. 在声明式环境记录中,不允许出现重复的标识符,所以它无法重复。甚至和var声明的标识符冲突。

3. 全局环境记录(Global Environment Record)

image.png 它是对一个对象式环境记录组件和一个声明式环境记录组件的封装;
全局环境的绑定对象就是window对象
全局环境记录的对象式环境记录组件,绑定了所有内置全局属性、全局的函数声明以及全局的var声明

(二)外部词法环境引用(Outer Lexical Environment)

  1. 全局环境的外部词法环境引用为null
  2. 类似于上层作用域,一直往外找直到全局环境记录(Global Environment Record),找不到就报错

二、变量环境

image.png 变量环境:标识其EnvironmentRecord(环境记录)保存在此执行上下文中由VariableStatements创建的绑定的词汇环境,故VariableEnvironment(变量环境)也是一个Lexical Environments(词法环境)。

在 ES6 中,LexicalEnvironment 组件和 VariableEnvironment 组件的区别在于前者用于存储函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var )绑定。

变量环境只有全局和函数作用域词法环境则是有全局、块、函数

三、案列(块级作用域)

function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()

let 关键字会创建块级作用域

第一步:编译并创建执行上下文 image.png

  • 函数内部通过var声明的变量,在编译阶段全都被存放到变量环境里面了。

  • 通过let声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。

  • 在函数的块级作用域内部,通过let声明的变量并没有被存放到词法环境中。 第二步:继续执行代码 image.png

  • 当执行到代码块里面时,变量环境中a的值已经被设置成了1,词法环境中b的值已经被设置成了2。

  • 当进入函数的块级作用域块时,作用域块中通过let声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量b,在该作用域块内部也声明了变量b,当执行到作用域内部时,它们都是独立的存在。

  • 在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量(通过let或者const声明),进入一个作用域块后,就会把该作用域块内部的变量((通过let或者const声明))压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构

第三步:打印

image.png

  • 当执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量a的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给JavaScript引擎,如果没有查找到,那么继续在变量环境中查找。

第四步:执行完后

image.png

  • 当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出

    console.log(d):Uncaught ReferenceError

  • 块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript引擎也就同时支持了变量提升和块级作用域了。

四、参考

1.【译】理解 Javascript 执行上下文和执行栈
2. 深入JavaScript系列(一):词法环境
3. 浏览器工作原理与实践