美文网首页
了解词法环境吗?它和闭包有什么联系?

了解词法环境吗?它和闭包有什么联系?

作者: an_371e | 来源:发表于2021-04-19 07:36 被阅读0次

    词法环境(Lexical Environment)

    官方定义

    官方 ES2020 这样定义词法环境(Lexical Environment):

    A Lexical Environment 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. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.

    词法环境是一种规范类型(specification type),它基于 ECMAScript 代码的词法嵌套结构,来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。

    说的很详细,可是很难理解喃🤔

    下面,我们通过一个 V8 中 JS 的编译过程来更加直观的解释。

    V8 中 JS 的编译过程来更加直观的解释

    大致分为三个步骤:

    • 第一步 词法分析 :V8 刚拿到执行上下文的时候,会把代码从上到下一行一行的进行分词/词法分析(Tokenizing/Lexing),例如 var a = 1; ,会被分成 vara1; 这样的原子符号((atomic token)。词法分析=指登记变量声明+函数声明+函数声明的形参。
    • 第二步 语法分析 :在词法分析结束后,会做语法分析,引擎将 token 解析成一个抽象语法树(AST),在这一步会检测是否有语法错误,如果有则直接报错不再往下执行
    var a = 1;
    console.log(a);
    a = ;
    // Uncaught SyntaxError: Unexpected token ;
    // 代码并没有打印出来 1 ,而是直接报错,说明在代码执行前进行了词法分析、语法分析
    
    • 注意: 词法分析跟语法分析不是完全独立的,而是交错运行的。也就是说,并不是等所有的 token 都生成之后,才用语法分析器来处理。一般都是每取得一个 token ,就开始用语法分析器来处理了
    • 第三步 代码生成 :最后一步就是将 AST 转成计算机可以识别的机器指令码

    在第一步中,我们看到有词法分析,它用来登记变量声明、函数声明以及函数声明的形参,后续代码执行的时候就可以知道要从哪里去获取变量值与函数。这个登记的地方就是词法环境。

    词法环境包含两部分:

    • 环境记录:存储变量和函数声明的实际位置,真正用来登记变量的地方
    • 对外部环境的引用:意味着它可以访问其外部词法环境,是作用域链能够连接起来的关键

    每个环境能访问到的标识符集合,我们称之为“作用域”。我们将作用域一层一层嵌套,形成了“作用域链”。

    词法环境有两种 类型 :

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

    环境记录 同样有两种类型:

    • 声明性环境记录 :存储变量、函数和参数。一个函数环境包含声明性环境记录。
    • 对象环境记录 :用于定义在全局执行上下文中出现的变量和函数的关联。全局环境包含对象环境记录。

    如果用伪代码的形式表示,词法环境是这样哒:

    GlobalExectionContext = {  // 全局执行上下文
      LexicalEnvironment: {       // 词法环境
        EnvironmentRecord: {        // 环境记录
          Type: "Object",              // 全局环境
          // ...
          // 标识符绑定在这里 
        },
        outer: <null>              // 对外部环境的引用
      }  
    }
    
    FunctionExectionContext = { // 函数执行上下文
      LexicalEnvironment: {       // 词法环境
        EnvironmentRecord: {        // 环境记录
          Type: "Declarative",         // 函数环境
          // ...
          // 标识符绑定在这里             // 对外部环境的引用
        },
        outer: <Global or outer function environment reference>  
      }  
    }
    

    例如:

    let a = 20;  
    const b = 30;  
    var c;
    
    function multiply(e, f) {  
     var g = 20;  
     return e * f * g;  
    }
    
    c = multiply(20, 30);
    

    对应的执行上下文、词法环境:

    GlobalExectionContext = {
    
      ThisBinding: <Global Object>,
    
      LexicalEnvironment: {  
        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>  
      }  
    }
    

    词法环境与我们自己写的代码结构相对应,也就是我们自己代码写成什么样子,词法环境就是什么样子。词法环境是在代码定义的时候决定的,跟代码在哪里调用没有关系。所以说 JS 采用的是词法作用域(静态作用域),即它在代码写好之后就被静态决定了它的作用域。

    静态作用域 vs 动态作用域

    动态作用域是基于栈结构,局部变量与函数参数都存储在栈中,所以,变量的值是由代码运行时当前栈的栈顶执行上下文决定的。而静态作用域是指变量创建时就决定了它的值,源代码的位置决定了变量的值。

    var x = 1;
    
    function foo() {
      var y = x + 1;
      return y;
    }
    
    function bar() {
      var x = 2;
      return foo();
    }
    
    foo(); // 静态作用域: 2; 动态作用域: 2
    bar(); // 静态作用域: 2; 动态作用域: 3
    

    在此例中,静态作用域与动态作用域的执行结构可能是不一致的,bar 本质上就是执行 foo 函数,如果是静态作用域的话, bar 函数中的变量 x 是在 foo 函数创建的时候就确定了,也就是说变量 x 一直为 1 ,两次输出应该都是 2 。而动态作用域则根据运行时的 x 值而返回不同的结果。

    所以说,动态作用域经常会带来不确定性,它不能确定变量的值到底是来自哪个作用域的。

    大多数现在程序设计语言都是采用静态作用域规则,如C/C++、C#、Python、Java、JavaScript等,采用动态作用域的语言有Emacs Lisp、Common Lisp(兼有静态作用域)、Perl(兼有静态作用域)。C/C++的宏中用到的名字,也是动态作用域。

    词法环境与闭包

    一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure

    ——MDN

    也就是说,闭包是由 函数 以及声明该函数的 词法环境 组合而成的

    var x = 1;
    
    function foo() {
      var y = 2; // 自由变量
      function bar() {
        var z = 3; //自由变量
        return x + y + z;
      }
      return bar;
    }
    
    var test = foo();
    
    test(); // 6
    

    基于我们对词法环境的理解,上述例子可以抽象为如下伪代码:

    GlobalEnvironment = {
      EnvironmentRecord: { 
        // 内置标识符
        Array: '<func>',
        Object: '<func>',
        // 等等..
    
        // 自定义标识符
        x: 1
      },
      outer: null
    };
    
    fooEnvironment = {
      EnvironmentRecord: {
        y: 2,
        bar: '<func>'
      }
      outer: GlobalEnvironment
    };
    
    barEnvironment = {
      EnvironmentRecord: {
        z: 3
      }
      outer: fooEnvironment
    };
    

    前面说过,词法作用域也叫静态作用域,变量在词法阶段确定,也就是定义时确定。虽然在 bar 内调用,但由于 foo 是闭包函数,即使它在自己定义的词法作用域以外的地方执行,它也一直保持着自己的作用域。所谓闭包函数,即这个函数封闭了它自己的定义时的环境,形成了一个闭包,所以 foo 并不会从 bar 中寻找变量,这就是静态作用域的特点。

    为了实现闭包,我们不能用动态作用域的动态堆栈来存储变量。如果是这样,当函数返回时,变量就必须出栈,而不再存在,这与最初闭包的定义是矛盾的。事实上,外部环境的闭包数据被存在了“堆”中,这样才使得即使函数返回之后内部的变量仍然一直存在(即使它的执行上下文也已经出栈)。

    每天三分钟,进阶一个前端小 tip
    面试题库
    算法题库

    相关文章

      网友评论

          本文标题:了解词法环境吗?它和闭包有什么联系?

          本文链接:https://www.haomeiwen.com/subject/eyeglltx.html