本文共 4772 字,大约阅读时间需要 15 分钟。
这样定义词法环境(Lexical Environment):
A Lexical Environment is a specification type used to define the association of s to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an and a possibly null reference to an outer Lexical Environment.
词法环境是一种规范类型(specification type),它基于 ECMAScript 代码的词法嵌套结构,来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。
说的很详细,可是很难理解喃🤔
下面,我们通过一个 V8 中 JS 的编译过程来更加直观的解释。
大致分为三个步骤:
var a = 1;
,会被分成 var
、 a
、 1
、 ;
这样的原子符号((atomic token)。词法分析=指登记变量声明+函数声明+函数声明的形参。var a = 1;console.log(a);a = ;// Uncaught SyntaxError: Unexpected token ;// 代码并没有打印出来 1 ,而是直接报错,说明在代码执行前进行了词法分析、语法分析
在第一步中,我们看到有词法分析,它用来登记变量声明、函数声明以及函数声明的形参,后续代码执行的时候就可以知道要从哪里去获取变量值与函数。这个登记的地方就是词法环境。
词法环境包含两部分:
每个环境能访问到的标识符集合,我们称之为“作用域”。我们将作用域一层一层嵌套,形成了“作用域链”。
词法环境有两种 类型 :
this
的值指向这个全局对象。arguments
对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。环境记录 同样有两种类型:
如果用伪代码的形式表示,词法环境是这样哒:
GlobalExectionContext = { // 全局执行上下文 LexicalEnvironment: { // 词法环境 EnvironmentRecord: { // 环境记录 Type: "Object", // 全局环境 // ... // 标识符绑定在这里 }, outer:// 对外部环境的引用 } }FunctionExectionContext = { // 函数执行上下文 LexicalEnvironment: { // 词法环境 EnvironmentRecord: { // 环境记录 Type: "Declarative", // 函数环境 // ... // 标识符绑定在这里 // 对外部环境的引用 }, outer: } }
例如:
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:, LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符绑定在这里 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符绑定在这里 c: undefined, } outer: } }FunctionExectionContext = { ThisBinding: , LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符绑定在这里 Arguments: {0: 20, 1: 30, length: 2}, }, outer: }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符绑定在这里 g: undefined }, outer: } }
词法环境与我们自己写的代码结构相对应,也就是我们自己代码写成什么样子,词法环境就是什么样子。词法环境是在代码定义的时候决定的,跟代码在哪里调用没有关系。所以说 JS 采用的是词法作用域(静态作用域),即它在代码写好之后就被静态决定了它的作用域。
动态作用域是基于栈结构,局部变量与函数参数都存储在栈中,所以,变量的值是由代码运行时当前栈的栈顶执行上下文决定的。而静态作用域是指变量创建时就决定了它的值,源代码的位置决定了变量的值。
var x = 1;function foo() { var y = x + 1; return y;}function bar() { var x = 2; return foo();}foo(); // 静态作用域: 2; 动态作用域: 2bar(); // 静态作用域: 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: '', Object: ' ', // 等等.. // 自定义标识符 x: 1 }, outer: null};fooEnvironment = { EnvironmentRecord: { y: 2, bar: ' ' } outer: GlobalEnvironment};barEnvironment = { EnvironmentRecord: { z: 3 } outer: fooEnvironment};
前面说过,词法作用域也叫静态作用域,变量在词法阶段确定,也就是定义时确定。虽然在 bar
内调用,但由于 foo
是闭包函数,即使它在自己定义的词法作用域以外的地方执行,它也一直保持着自己的作用域。所谓闭包函数,即这个函数封闭了它自己的定义时的环境,形成了一个闭包,所以 foo
并不会从 bar
中寻找变量,这就是静态作用域的特点。
为了实现闭包,我们不能用动态作用域的动态堆栈来存储变量。如果是这样,当函数返回时,变量就必须出栈,而不再存在,这与最初闭包的定义是矛盾的。事实上,外部环境的闭包数据被存在了“堆”中,这样才使得即使函数返回之后内部的变量仍然一直存在(即使它的执行上下文也已经出栈)。
本文首发自「三分钟学前端」,回复「交流」自动加入前端三分钟进阶群,每日一道编程算法面试题(含解答),助力你成为更优秀的前端开发!
转载地址:http://ocbsi.baihongyu.com/