带你彻底搞明白JS运行时 - 词法环境(Lexical Environment)

864 阅读16分钟

前言

本篇基于ECMAScript 5(ES5)规范,带你彻彻底底搞明白到底什么是词法环境(Lexical Environment)。注意:本篇只讲词法环境,关于执行上下文,我会另开一章。本文纯手敲,如果大家发现了文中有漏洞也欢迎大家在评论区进行指正。

先打个比方

为了能让大家更通俗的阅读本文,先假设我们正在参加一个会议,会议的组织结构就像是词法环境,而会议进行的过程就像是执行上下文

  1. 全局词法环境:可以类比为整个会议的基本信息,包括会议名称、会议日期、参会人员名单等。这些信息对所有参会者都是可见的。
  2. 函数词法环境:可以想象为会议中的一个小组讨论。在小组讨论中,有自己的讨论主题、参与讨论的人员名单、讨论所需的资料等。这些信息在小组讨论内部是可见的,但对其他小组是不可见的。
  3. 块级词法环境:在小组讨论中,可能还会有一些临时的子话题。这些子话题仅在特定的时间段内进行讨论,类似于块级作用域。参与子话题讨论的人员、讨论内容等信息仅在该子话题讨论时是可见的。

现在,我们来看一下执行上下文的类比。在会议进行的过程中,每个参与者都处于一个特定的环境中。例如,在全体会议环节,所有参与者都在全局执行上下文中。当进行小组讨论时,参与者进入了一个新的执行上下文(函数执行上下文)。在小组讨论中,还可能有临时的子话题讨论环节,这时参与者又进入了一个新的执行上下文(类似于块级词法环境)。

一、概念

词法环境(Lexical Environment)

  • 大家可能对词法环境(Lexical Environment)这个词又陌生又熟悉,但是一定对作用域(scope)这个词不陌生。你可以跟着我的思路,把词法环境(Lexical Environment)就想象成ES3规范中的作用域(scope),在ECMAScript 5(ES5)规范中,你可以理解为,只是这个作用域(scope)这个词换了个叫法,变成了词法环境(Lexical Environment)而已。
  • 首先,词法环境是静态的。一定要记住,他是静态的,不要和执行上下文(Execution context)搞混。词法环境是一个存储变量和函数声明的结构,它包含了一个环境记录[Environment Record](用于存储变量和函数声明)和一个外部词法环境引用(Outer Lexical Environment Reference),(即作用域链)。词法环境用于解析标识符(例如变量名)并获取它们的值。
  • 你可以把词法环境抽象成一个大类,这个大类中分为了3个子类,如下。
    1. 全局词法环境(Global Lexical Environment):在全局作用域下,包含了全局变量、函数声明等。
    2. 函数词法环境(Function Lexical Environment):在函数作用域下,包含了函数内部的变量、函数参数、内部函数声明等。
    3. 块级词法环境(Block Lexical Environment):在块级作用域下,包含了由letconst声明的变量、块级函数声明等。
  • 上图

d94f5b272d493f7cd9ed8a3750dd0c2.png

词法环境实例(Lexical Environment Instance)

  • 词法环境实例(Lexical Environment Instance)大家是不是很少听到这个名词,它其实是词法环境(Lexical Environment) 的一个具体实例。比如每次函数被调用或者进入一个新的作用域时,都会创建一个新的词法环境实例。这个实例包含了当前作用域内的变量和函数,以及一个指向外部(父级)词法环境的引用。

  • 正如你所想。词法环境实例也分为3种,如下。

    1. 全局词法环境实例(Global Lexical Environment Instance)
    2. 函数词法环境(Function Lexical Environment Instance)
    3. 块级词法环境(Block Lexical Environment Instance)

    例如,以下代码在全局词法环境和块级词法环境中都声明了变量:

    let x = 10; // 全局词法环境实例
    
    if (true) {
      let y = 20; // 块级词法环境实例
    }
    

    在这个例子中,全局词法环境实例包含了变量 x,而块级词法环境实例包含了变量 y。

    词法环境实例和词法环境你可以理解为,词法环境是抽象的,而词法环境实例则是这个抽象的具体实现。

环境记录(Environment Record)

  • 环境记录(Environment Record)是一个用于存储变量、函数声明和其他标识符绑定的数据结构。这两种环境都包含了一个环境记录,上面说的词法环境,就是由一个环境记录和一个外部引用所组成的。然而环境记录你也可以和上面描述词法环境一样,把环境记录抽象成一个大类,然后这大类中,有2个子类,如下。

    1.声明性环境记录(Declarative Environment Record) :这种类型的环境记录是用于存储函数和变量声明的。它们通常被用于函数作用域和块作用域。

    2.对象环境记录(Object Environment Record) :这种类型的环境记录基于一个实际的对象来处理。它们用于存储由var声明的变量和函数声明,以及全局环境中的全局对象。

  • 到这里,可能有朋友会有疑问了。不对啊,和我了解的有点冲突呢,应该是有3个子类吧,分别是:声明性环境记录(Declarative Environment Record),对象环境记录(Object Environment Record),全局环境记录 (Global Environment Record)才对。其实,所说的这个全局环境记录(Global Environment Record) 确确实实存在,但其实它是一个特殊的环境记录。全局环境记录中,包含了声明性环境记录和对象环境记录。所以我在上面没有把全局环境(Global Environment Record)记录归入环境记录(Environment Record) 这个大类中。全局环境(Global Environment Record)在创建时,会生成一个新的对象环境记录,其关联对象就是全局对象。然后,所有的全局变量和函数都会成为这个全局对象的属性,因此可以在全局范围内访问。如果还是不理解,那就直接就把全局环境记录(Global Environment Record) 抽象成一个对象,这个对象就是一个根对象。

  • 上图

image.png

外部环境引用(Outer Environment Reference)

  • 外部环境引用(Outer Environment Reference)是指向词法环境的父级作用域的引用。它用于实现作用域链,使得 代码可以访问到外部(父级)作用域中的变量和函数。

二、每个概念的详解

词法环境详解

全局词法环境(Global Lexical Environment)

  • 全局词法环境是 JavaScript 代码开始执行时创建的词法环境。全局词法环境包含了所有在全局中声明的变量和函数。在全局词法环境中,this 值通常指向全局对象(在浏览器中是 window 对象,在 Node.js 中是 global 对象)。全局词法环境外部环境引用(Outer Environment Reference)null,因为它没有任何外部环境。
  • 上图

81273d702c4b10e0abbf55074bdd61e.png

用代码展示一下

  let x = 100;
  function fun() {
    console.log(x);
  }

在这个例子中,全局词法环境会包含 x 和 fun。

函数词法环境(Function Lexical Environment)

  • 函数词法环境在JavaScript 中是当一个函数被调用时创建的词法环境。它包含了函数的参数,以及在函数体内声明的变量和函数。 在函数词法环境中,this 值取决于函数是如何被调用的。函数词法环境外部环境引用(Outer Environment Reference) 通常指向创建这个函数的词法环境
  • 在全局词法环境中声明的函数如下

image.png

  • 在函数词法环境中声明的函数如下 image.png

    用代码举个例子

     let x = 10; // 全局词法环境
     function fun(y) { // 函数词法环境
       let z = 30; // 函数词法环境
       console.log(x + y + z); // 可以访问到全局词法环境和函数词法环境的变量
     }
    
     fun(20); // 调用函数,创建函数词法环境
    

    在这个例子中,当 fun 函数被调用时,会创建一个新的函数词法环境,其中包含了参数 y 和在函数体内声明的变量 z。这个函数词法环境的外部环境引用(Outer Environment Reference) 指向全局词法环境,因此 fun 函数可以访问到全局变量 x。

块级词法环境(Block Lexical Environment)

  • 块级词法环境(Block Lexical Environment)在 JavaScript 中是当进入一个新的块级作用域(如 if 语句、for 循环或者 {} 块)时创建的词法环境。它包含了在这个块级作用域内声明的变量和函数。在块级词法环境中,let 和 const 声明的变量只在这个块级作用域内可见。块级词法环境的外部环境引用(Outer Environment Reference) 通常指向创建这个块级作用域的词法环境。
  • 这里就不上图了(主要作图有点麻烦),其实和上面的函数词法环境差不多。

    用代码举个例子

    if (true) {
      let y = 20; // 块级词法环境
      console.log(y);
    }
    

    在这个例子中,当进入 if 语句块时,会创建一个新的块级词法环境,这个环境包含了变量 y。

环境记录详解

声明性环境记录(Declarative Environment Record)

  • 声明性环境记录(Declarative Environment Record)是 JavaScript 中用于存储变量和函数声明的一种环境记录类型。它用于存储 var,let,const 变量声明,以及函数声明。在声明性环境记录中,每个声明的标识符都有一个与之关联的值,以及一组属性,比如,是否可写(对于 const 声明的变量,其可写属性为 false)。

    用代码举个例子

      let x = 10;
    
      function fun() {
        console.log(x);
      }
    

    在这个例子中,声明性环境记录包含了变量 x 和函数 fun 的声明。

对象环境记录(Object Environment Record)

  • 对象环境记录(Object Environment Record)是 JavaScript 中用于存储通过对象创建的环境的一种环境记录类型。对象环境记录主要用于 with 语句和全局对象。例如,当你在全局作用域中声明一个变量,它实际上是在全局对象(在浏览器中是 window 对象)上创建了一个属性。

    用代码举个例子

    var x = 10;
    

    在这个例子中,全局对象的环境记录包含了属性 x 的声明。

总结一下(声明性环境记录)和(对象环境记录)的区别

  • 声明性环境记录(Declarative Environment Record)和对象环境记录(Object Environment Record)都是 JavaScript 中用于存储变量和函数声明的环境记录,但它们有以下的4个区别:

    1. 存储内容:声明性环境记录用于存储 var,let,const 变量声明,以及函数声明。而对象环境记录主要用于 with 语句和全局对象。

    2. 创建方式:声明性环境记录通常在进入一个新的词法环境(如函数或块级作用域)时创建。而对象环境记录是通过对象创建的,例如全局对象或 with 语句创建的对象。

    3. 访问方式:声明性环境记录中的变量和函数可以直接通过它们的名称访问。而对象环境记录中的变量和函数实际上是对象的属性,需要通过对象来访问。

    4. 属性特性:声明性环境记录中的每个标识符都有一组属性,如是否可写、是否可配置。而对象环境记录中的属性则具有对象属性的所有特性,如可枚举性、可配置性和可写性。

函数环境记录(Function Environment Record)

  • 函数环境记录(Function Environment Record)是一种特殊的声明性环境记录(Declarative Environment Record),它用于存储函数声明以及函数的参数。它除了包含声明性环境记录的所有内容外,还包含了一些特有的元素,如 this 值和 arguments 对象。当一个函数被调用时,会创建一个新的函数环境记录。这个环境记录包含了函数的参数,以及在函数体内声明的变量和函数。同时,this 值和 arguments 对象也会被添加到这个环境记录中。

    用代码举个例子

    function fun(y) { // 函数环境记录
    let z = 30; // 函数环境记录
    console.log(this, arguments, y, z);
      }
    
    fun(20); // 调用函数,创建函数环境记录
    

    在这个例子中,当 fun 函数被调用时,会创建一个新的函数环境记录,其中包含了参数 y,在函数体内声明的变量 z,以及 this 值和 arguments 对象。

模块环境记录(Module Environment Record)

  • 模块环境记录(Module Environment Record)也是一种特殊的声明性环境记录,它用于存储 ES6 模块中的变量和函数声明。当一个 ES6 模块被加载和执行时,会创建一个新的模块环境记录。

  • 模块环境记录与普通的声明性环境记录(Declarative Environment Record)有一些重要的区别:
    1. 导出的绑定:模块环境记录包含了模块导出的所有绑定(即通过 export 语句导出的变量和函数)。
    2. 不可变的绑定:模块环境记录中的绑定是不可变的,即不能给一个已经存在的导出绑定赋予新的值。这是因为 import 语句创建的是一个只读的引用到导出的值。

    用代码举个例子

    // myModule.js
      export let x = 10;
      export function foo() {
        console.log(x);
      }
    

    在这个例子中,模块环境记录包含了变量 x 和函数 foo 的导出绑定。

全局环境记录(Global Environment Record)

  • 全局环境记录(Global Environment Record)是 JavaScript 中的一种特殊环境记录,它用于存储全局作用域中的变量和函数声明。全局环境记录在 JavaScript 运行时被创建,并且在整个程序执行期间一直存在。全局环境记录实际上是由两部分组成的:一个是声明性环境记录,一个是对象环境记录。声明性环境记录用于存储通过 var 声明的全局变量和函数声明,而对象环境记录则包含了全局对象的所有属性。在浏览器中,全局对象通常是 window 对象。

    用代码举个例子

      // 全局变量,会被添加到全局环境记录的声明性环境记录部分
      var x = 10;
    
      // 全局函数,也会被添加到全局环境记录的声明性环境记录部分
      function fun() {
        console.log(x);
      }
    
      // 通过给全局对象(在浏览器中是 window 对象)添加属性,这个属性会被添加到全局环境记录的对象环境记录部分
      window.y = 20;
    

    在这个例子中,全局环境记录包含了变量 x 和函数 fun 的声明(在声明性环境记录部分),以及全局对象的属性 y(在对象环境记录部分)。到这里会不会有个疑惑? var x = 10; 是被记录在了声明性环境记录里面。 那window.x 也是可以访问的啊。 那是因为当我们在全局作用域中使用 var 声明变量时,这个变量会被添加到声明性环境记录中。同时,由于全局对象(在浏览器中是 window 对象)是全局环境记录的对象环境记录,所以这个变量也会作为全局对象的属性存在。

  • 通过 let 和 const 声明的全局变量只会被添加到全局环境记录的声明性环境记录中,而不会被添加到对象环境记录中。这意味着,你不能通过全局对象(在浏览器中是 window 对象)来访问这些变量。

    用代码举个例子

     let x = 10; // 这个变量只存在于全局环境记录的声明性环境记录中
     console.log(window.x); // 输出:undefined
    
  • 在全局作用域中,只有通过 var 声明的变量会同时被添加到全局环境记录声明性环境记录对象环境记录中。而通过 let、const 或者 class 声明的全局变量只会被添加到声明性环境记录中。这就是为什么我们不能通过全局对象(在浏览器中是 window 对象)来访问通过 let、const 或者 class 声明的全局变量的原因。

三、总结

词法环境和5个环境记录。并且知道了,词法环境是由环境记录和外部引用所构成的。但你有没有想过一个问题。难道这3个词法环境(全局词法环境,函数词法环境,块级词法环境)分别都包含了这5个环境记录(全局环境记录, 声明性环境记录,对象环境记录,函数环境记录,模块环境记录)吗?我们用代码看一下,到底是个啥情况。

// 全局词法环境
  var x = 10; // 全局环境记录(声明性环境记录和对象环境记录)

  function fun() { // 函数词法环境
    var y = 20; // 函数环境记录
    if (true) { // 块级词法环境
      let z = 30; // 声明性环境记录
    }
  }

  // ES6 模块
  export const a = 1; // 模块环境记录

在这个例子中,全局变量 x 存储在全局环境记录中,函数 fun 的局部变量 y 存储在函数环境记录中,块级作用域的变量 z 存储在声明性环境记录中,模块的导出 a 存储在模块环境记录中。其实每个词法环境中的环境记录都是不一样的,比如函数词法环境中就没有全局环境记录。虽然没有全局环境记录,但是我可以通过外部引用访问到全局词法环境,因此可以访问到全局变量,上代码

// 全局词法环境
var globalVar = 10; // 存储在全局环境记录中

function fun() { // 函数词法环境
  var localVar = 20; // 存储在函数环境记录中
  console.log(globalVar) // 输出 10
}
 > 在这个例子中,全局变量 globalVar 存储在全局环境记录中,而函数 fun 的局部变量 localVar 存储在函数环境记录中。函数词法环境并不包含全局环境记录,但是它可以通过外部引用访问到全局词法环境,因此可以访问到全局变量 globalVar。

希望大家可以通过本篇所讲有所收获。如果感觉不错,劳烦各位点点赞,谢谢!!!