JavaScript筑基(三):环境记录

1,129 阅读14分钟

环境记录

词法环境 Lexical Enviroment 是词法作用域这一概念的运行期实现:代码在物理行上、显式的进行形式分块,并在引擎执行期映射成环境记录 Environment Record ,以便将“块级别的”词法作用域实例化

在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:

  • this binding ,即我们所熟知的 this 绑定
  • 创建词法环境 LexicalEnviroment 组件。
  • 创建变量环境 VariableEnviroment 组件。

上节介绍作用域之后,又回到了我们的执行上下文。这次详细分析执行上下文创建阶段所实现的词法环境变量环境,关于this的绑定规则会出另一节尽可能详细的解释。

词法环境变量环境分别对应了词法作用域变量作用域的内部实现,因此上一节最后的总结主要提及了两种作用域。

虽然在ES2019中新增了私有域PrivateEnvironment,但是这个后续再议。

规范参考

9.4 Execution Contexts

Execution contexts for ECMAScript code have the additional state components

ComponentPurpose
LexicalEnvironmentIdentifies the Environment Record used to resolve identifier references made by code within this execution context.
VariableEnvironmentIdentifies the Environment Record that holds bindings created by VariableStatements within this execution context.
PrivateEnvironmentIdentifies the PrivateEnvironment Record that holds Private Names created by ClassElements in the nearest containing class. null if there is no containing class.

这节只讨论LexicalEnvironmentVariableEnvironmentPrivateEnvironment以后再进行讨论

其中对词法环境的解释:

Identifies the Environment Record used to resolve identifier references made by code within this execution context.

标识用于解析此执行上下文中代码所做的标识符引用的环境记录 Environment Record

还没开始分析呢,第一个大问题就来了。这个 Environment Record是什么?为什么词法环境是这个东西?

环境记录

我们先来看看什么是环境记录

9.1 Environment Records

Environment Record is a specification type used to define the association of Identifiers to specific variables and functions, based upon the lexical nesting structure of ECMAScript code. Usually an Environment Record is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement. Each time such code is evaluated, a new Environment Record is created to record the identifier bindings that are created by that code.

Environment Record是一种规范类型,基于ECMAScript代码的词法嵌套结构,用于定义标识符与特定变量和函数的关联。通常,环境记录与ECMAScript代码的一些特定语法结构相关联,比如函数声明、BlockStatement(块)或TryStatement的Catch子句。每次计算这样的代码时,都会创建一个新的Environment Record来记录由该代码创建的标识符绑定。

只看重点,环境记录定义了标识符var,let,function...声明的变量名)与特定变量和函数关联

通常有三种创建时机

  • 函数声明:最开始执行的script,以及普通的函数声明
  • 块语句:如 {} 所包裹的
  • try-catch 中的 catch 语句

那这个东西到底有什么用呢?

环境记录可以理解为一种标识符<—>变量映射表(字典),当我们需要调用变量的时候,就在这里面查找对应的值。

环境记录的分类

Environment Records can be thought of as existing in a simple object-oriented hierarchy where Environment Record is an abstract class with three concrete subclasses: declarative Environment Record, object Environment Record, and global Environment Record. Function Environment Records and module Environment Records are subclasses of declarative Environment Record.

环境记录可以分为三类:

  • 声明式环境记录declarative(可继续细分为两类)

    • 函数环境记录 Function
    • 模块环境记录 module
  • 对象式环境记录 object

  • 全局环境记录 global

声明式环境记录declarative Environment Record

Each declarative Environment Record is associated with an ECMAScript program scope containing variable, constant, let, class, module, import, and/or function declarations.

储存了作用范围中关于变量,const、let、class....的声明

可以用伪代码这么理解

 var foo = 123
 const name = "Daming"
 function bar() {}
 ​
 // declarative Environment Record
 {
     foo : 123
     name : "Daming"
     bar : <function object>
 }

函数环境记录 Function Environment Record

if the function is not an ArrowFunction, provides a this binding.

If a function is not an ArrowFunction function and references super, its function Environment Record also contains the state that is used to perform super method invocations from within the function.

用于保存外层函数的定义。如果当前函数不是一个箭头函数,就提供一个this绑定。提供了super关键字。

模块环境记录 module Environment Record

In additional to normal mutable and immutable bindings, module Environment Records also provide immutable import bindings which are bindings that provide indirect access to a target binding that exists in another Environment Record.

这种环境记录除了普通的可变和不可变的绑定外,还提供了不可变的导入绑定。这些绑定提供了对存在于另一个环境记录中的目标绑定的间接访问

对象式环境记录Object Environment Record

An object Environment Record binds the set of string identifier names that directly correspond to the property names of its binding object.

绑定了一组字符串标识符名称,这些名称直接对应于其绑定对象的属性名称。就是保存对象的key-value

 global: {
     name: "Sam",
     age: 10
 }
 ​
 // Object Environment Record
 {
     "name" : "Sam"
     "age" : 10
 }

不过,它只记录通过 var 声明的标识符。也因为它绑定了一个对象,这个声明后的标识符,会绑定到绑定对象同名属性

也有一种说法,对象式环境记录是 variable object 实现的规范。

全局环境记录项 global environment records

A global Environment Record is used to represent the outer most scope that is shared by all of the ECMAScript Script elements that are processed in a common realm. A global Environment Record provides the bindings for built-in globals (clause 19), properties of the global object, and for all top-level declarations (8.1.9, 8.1.11) that occur within a Script.

A global Environment Record is logically a single record but it is specified as a composite encapsulating an object Environment Record and a declarative Environment Record.

The object Environment Record has as its base object the global object of the associated Realm Record.

表示最外层script标签所包裹的代码环境,这个环境记录项目里面包含了JS内置对象的属性,全局对象的属性以及所有在script中的顶级声明。

在逻辑上,他是一个独立的记录。但实际上它被指定为封装了对象式环境记录声明式环境记录的一个复合记录

它的基础对象是相关领域记录的全局对象

因为它绑定了全局对象,所以在浏览器中,window 就是它所绑定的对象 。而绑定这个实际存在的对象,需要通过对象式环境记录。 并且在规范中,它有 [[ObjectRecord]][[DeclarativeRecord]] 字段指向两种记录

综上可以得到一个简单的结论,它实际上就是声明式环境记录对象式环境记录的缝合。

此外,它还有字段 [[VarNames]] 用于联系一些关键词在全局声明的变量 function,generator,async,var....

The string names bound by FunctionDeclaration, GeneratorDeclaration, AsyncFunctionDeclaration, AsyncGeneratorDeclaration, and VariableDeclaration declarations in global code for the associated realm.

说到这里,我们就可以解释一下为什么使用 var 声明的变量在浏览器中可以通过window 调用,而let/const声明的就不可以。

上文提到对象式环境记录会记录 var 声明的标识符,同时绑定到绑定对象的同名属性上,在这里,绑定对象就是 window 。即如果在全局环境中使用 var 来声明,就会通过全局环境记录指向的对象式环境记录绑定到 window 上,所以我们可以通过 window 来访问用 var 声明的变量。

如果还是觉得很抽象,在文末会通过伪代码来看看是怎么实现的。

环境记录的 [[OuterEnv]]

Every Environment Record has an [[OuterEnv]] field, which is either null or a reference to an outer Environment Record. The outer reference of an (inner) Environment Record is a reference to the Environment Record that logically surrounds the inner Environment Record. An outer Environment Record may, of course, have its own outer Environment Record. An Environment Record may serve as the outer environment for multiple inner Environment Records.

每个环境记录都有一个[[OuterEnv]]字段,它要么是空的,要么是对外部环境记录的引用。

一个环境记录的外部参考,在逻辑上,是包裹这条内部环境记录的环境记录的参考

一个外部的环境记录也可以拥有它自己的外部环境记录,一个环境记录可以作为多个内部环境记录的外部环境

说人话:通过[[OuterEnv]]当前的环境记录可以访问到最近的外部环境记录,如果是全局词法环境,这个值为null。也就是它,把不同的代码段串成了链式结构。

是不是有种熟悉的味道。是的,就是这个概念形成了新的作用域链 scope chain 的核心。

环境记录的作用

总结一下这个概念:

  • 在一些特定代码执行的时候,会创建环境记录
  • 这个环境记录里面储存了当前环境中标识符和实际引用的映射,当代码的运行需要某个变量的时候,会通过环境记录来获取对应的值
  • 如果当前的环境记录没有需要的值,那就会通过[[OuterEnv]] 从外部环境记录寻找

到此可以看出,环境记录里面不仅可以存储变量,也有访问外部环境的指针,在函数环境记录中顺便还把this绑定给添上了。

执行上下文经典三件套,齐活。也因此就把es3这套 作用域链 + variable object / activation object 的概念给取代了。

曾经的概念看起来更简单易懂,那为什么还要用 Environment Record 取代 variable object 呢?

在这之前先要了解一个前提。ES3中的 variable object 可以理解为一个普通对象的实现,类似于环境记录中的对象式环境记录,每有一个变量被声明,就会往这个对象上添加对应的属性。而现在使用的词法环境变量环境,使用的是声明式环境记录

所以总结一下主要有两点原因:1. 性能 2. 支持新特性

  • 声明式环境记录declarative environment records可能允许使用完整的词法寻址技术 lexical addressing ,即直接访问需要的变量,而不需要通过任何作用域链查找,无论其嵌套深度如何都不会影响性能。
  • In addition to the mutable bindings supported by all Environment Records, declarative environment records also provide for immutable bindings. An immutable binding is one where the association between an identifier and a value may not be modified once it has been established.

    声明式环境记录还提供了不可变绑定。以便于支持关键字 const 的特性

  • as Brendan Eich also mentioned (the last paragraph) — the activation object implementation in ES3 was just “a bug”: “I will note that there are some real improvements in ES5, in particular to Chapter 10 which now uses declarative binding environments. ES1-3’s abuse of objects for scopes (again I’m to blame for doing so in JS in 1995, economizing on objects needed to implement the language in a big hurry) was a bug, not a feature” .

    ES3时期及之前的实现,对象作用域的滥用只是个bug,会浪费性能,而不是一个特性

词法环境

饶了这么一大圈,我们终于可以解释词法环境是个什么东西了

再来看一次规范的定义

Identifies the Environment Record used to resolve identifier references made by code within this execution context.

结合环境记录的概念,可以理解为词法环境其实执行上下文被创建的时候,可以为执行上下文服务的环境记录。里面有变量,有this,也有可以指向外部环境用于形成作用域链的 [[outerEnv]]

变量环境

Identifies the Environment Record that holds bindings created by VariableStatements within this execution context.

用于储存 var 声明的变量

这也是个环境记录,但在实践中,只是用于储存 var 声明的变量。

对应的,词法环境中,记录了非 var 声明的标识符,比如let/const、function...

显而易见的是,var 声明的变量和 let/const 声明的变量是在不同的环境记录里面的。上一节《作用域》里面提到,ECMA在制定新规范的时候趁机把 var 这个历史遗留问题全都归纳到了变量作用域上。(这个观点出自《JavaScript语言精髓与编程实践》)

同时,我们也提到了,变量环境是变量作用域的运行时实现,所以这两点可以说是相互印证的。

因为这块内容是属于规范中抽象的概念,规范只是游戏规则,并不真正设计游戏。各方都有不同的理解,但如果在一定的限制下可以自圆其说,那也就是正确的。

与块级作用域

回忆一下什么时候会创建执行上下文,是某个函数被执行的时候。

那么显然,运行块级作用域中的代码并不会创建执行上下文。但是在环境记录的定义中,遇到BlockStatement,会创建一个新的环境记录,用于管理块中的变量。

事实上,块级作用域中用 let/const 声明的代码只在块中可见,所以应该存在一个环境记录用于管理新声明的变量或函数。这个新的环境记录就是函数的内部词法环境LexicalEnvironment ,它可以管理 let/const 声明的变量。换句话说,由let/const声明的变量会被最近的词法环境所管理。

通过 var 声明的变量不能由词法环境管理,因此会逸出到最近的变量环境上。

举个例子

这里举个例子来形象的看看词法环境变量环境的关系,以及它们 [[outerEnv]]指向问题

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

当我们调用这个函数的时候,它会创建对应的执行上下文ExecutionContext

同时为了方便展示指向,我们假设两种环境都存储到一个实际的堆内存中。

 ExecutionContext: {
     LexicalEnvironment[0x01]: {
         b -> nothing
         outerEnv: VariableEnvironment[0x02]
     }
     
     VariableEnvironment[0x02]: {
         a -> undefined, c -> undefined
         outerEnv: global
     }
     ...
 }

当代码执行到 if 语句的块中,会创建一个新的词法环境

 ExecutionContext: {
     LexicalEnvironment[0x03]: {
         d -> nothing
         outerEnv: LexicalEnvironment[0x01]
     }
     
     LexicalEnvironment[0x01]: {
         b -> 2
         outerEnv: VariableEnvironment[0x02]
     }
     
     VariableEnvironment[0x02]: {
         a -> 1, c -> undefined
         outerEnv: global
     }
     ...
 }

最后离开这个块,则会删除对应的词法环境

 ExecutionContext: {
     LexicalEnvironment[0x01]: {
         b -> 2
         outerEnv: VariableEnvironment[0x02]
     }
     
     VariableEnvironment[0x02]: {
         a -> 1, c -> 3
         outerEnv: global
     }
     ...
 }

再举个例子

基于上个例子的理解,我们可以来看看全局环境下var的变量是怎么管理的

 var a = 1;
 let b = 2;
 function foo() {}

假设上述代码块是在全局环境中,且在浏览器环境下的

 GlobalExecutionContext: {
     LexicalEnvironment[0x01]: {
         b -> 2, foo -> <function object>
         outerEnv: VariableEnvironment[0x02]
     }
     
     VariableEnvironment[0x02]: {
         a -> 1
         outerEnv: null
     }
     
     GlobalEnvironmentRecord[0x00]: {
         [[DeclarativeRecord]]: 0x01,
         [[ObjectRecord]]: { // 与 window 对象绑定
             a : 1
             ...
         },
         [[VarNames]]: ["a", "b", "foo",...]
     }
     ...
 }

总结

说到最后,如果例子里面不提一下创建了执行上下文,可能都忘了环境记录是执行上下文里面的东西了,那么这两个环境记录,在执行上下文中起到了什么作用呢?

执行上下文有个栈,我们每次遇到新的函数块都会新建对于的上下文并且压入栈中。环境记录保证了JavaScript静态作用域的实现,即我们可以通过变量环境中的[[outerEnv]] 访问到外部环境,无论当前运行的执行上下文在执行上下文栈中处于什么位置,都能准确地访问到函数在被声明时的外部环境

参考资料

《JavaScript语言精髓与编程实践》(第三版)

《JavaScript忍者秘籍》(第二版)

ECMAScript® 2022 Language Specification

【JS】详解ES6执行时词法环境/作用域/执行上下文/执行栈和闭包

精读Javascript系列(二)环境记录与词法环境

Why do we need VariableEnvironment to identify the state of an Execution Context in Javascript? - Stack Overflow

Why variable object was changed to lexical environment in ES5? - Stack Overflow

What really is a declarative environment record and how does it differ from an activation object? - Stack Overflow

Variable Environment vs lexical environment - Stack Overflow

最后

作者水平有限,如果有错误或者不严谨的地方,请务必指出,十分感谢!!!