词法环境(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;
,会被分成var
、a
、1
、;
这样的原子符号((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; 动静作用域: 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: '<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
面试题库
算法题库