Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
sisterAn opened this issue Apr 6, 2021 · 0 comments
Open

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

sisterAn opened this issue Apr 6, 2021 · 0 comments

Comments

@sisterAn
Copy link
Owner

sisterAn commented Apr 6, 2021

词法环境(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 中寻找变量,这就是静态作用域的特点。

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant