ECMAScript规范中对词法环境的描述以下:词法环境是用来定义 基于词法嵌套结构的ECMAScript代码内的标识符与变量值和函数值之间的关联关系 的一种规范类型。一个词法环境由环境记录(Environment Record)和一个可能为
null
的对外部词法环境的引用(outer)组成。通常来讲,词法环境都与特定的ECMAScript代码语法结构相关联,例如函数、代码块、TryCatch
中的Catch
从句,而且每次执行这类代码时都会建立新的词法环境。git
简而言之,词法环境就是相应代码块内标识符与值的关联关系的体现。若是以前了解过做用域概念的话,和词法环境是相似的(ES6以后做用域概念变为词法环境概念)。github
词法环境有两个组成部分:浏览器
环境记录(Environment Record):记录相应代码块的标识符绑定。函数
能够理解为相应代码块内的全部变量声明、函数声明(代码块若为函数还包括其形参)都储存于此
对应ES6以前的变量对象or活动对象,没了解过的可忽略ui
对外部词法环境的引用(outer):用于造成多个词法环境在逻辑上的嵌套结构,以实现能够访问外部词法环境变量的能力。this
词法环境在逻辑上的嵌套结构对应ES6以前的做用域链,没了解过的可忽略spa
环境记录有三种类型,分别是声明式环境记录(Declarative Environment Record)、对象式环境记录(Object Environment Record)、全局环境记录(Global Environment Record)。3d
声明式环境记录是用来定义那些直接将标识符与语言值绑定的ES语法元素,例如变量,常量,let,class,module,import以及函数声明等。code
声明式环境记录有函数环境记录(Function Environment Record)和模块环境记录(Module Environment Record)两种特殊类型。cdn
函数环境记录用于体现一个函数的顶级做用域,若是函数不是箭头函数,还会提供一个this
的绑定。
模块环境记录用于体现一个模块的外部做用域(即模块export所在环境),除了正常绑定外,也提供了全部引入的其余模块的绑定(即import的全部模块,这些绑定只读),所以咱们能够直接访问引入的模块。
每一个对象式环境记录都与一个对象相关联,这个对象叫作对象式环境记录的binding object
。能够理解为对象式环境记录就是基于这个binding object
,以对象属性的形式进行标识符绑定,标识符与binding object
的属性名一一对应。
是对象就能够动态添加或者删除属性,因此对象环境记录不存在不可变绑定。
对象式环境记录用来定义那些将标识符与某些对象属性相绑定的ES语法元素,例如with语句、全局var声明和函数声明。
全局环境记录逻辑上来讲是单个记录,可是实际上能够看做是对一个对象式环境记录
组件和一个声明式环境记录
组件的封装。
以前说过每一个对象式环境记录
都有一个binding object
,全局环境记录的对象式环境记录
的binding object
就是全局对象,在浏览器内,全局的this
及window
绑定都指向全局对象。
全局环境记录的对象式环境记录
组件,绑定了全部内置全局属性、全局的函数声明以及全局的var
声明。
因此这些绑定咱们能够经过window.xx
或this.xx
获取到。
全局代码的其余声明(如let、const、class等)则绑定在声明式环境记录
组件内,因为声明式环境记录
组件并非基于简单的对象形式来实现绑定,因此这些声明咱们并不能经过全局对象的属性来访问。
首先要说明两点:
null
。外部词法环境的引用将一个词法环境和其外部词法环境连接起来,外部词法环境又拥有对其自身的外部词法环境的引用。这样就造成一个链式结构,这里咱们称其为环境链(即ES6以前的做用域链),全局环境是这条链的顶端。
环境链的存在是为了标识符的解析,通俗的说就是查找变量。首先在当前环境查找变量,找不到就去外部环境找,还找不到就去外部环境的外部环境找,以此类推,直到找到,或者到环境链顶端(全局环境)还未找到则抛出ReferenceError
。
标识符解析:在环境链中解析变量(绑定)的过程,
咱们使用伪代码来模拟一下标识符解析的过程。
ResolveBinding(name[, LexicalEnvironment]) {
// 若是传入词法环境为null(即一直解析到全局环境还未找到变量),则抛出ReferenceError
if (LexicalEnvironment === null) {
throw ReferenceError(`${name} is not defined`)
}
// 首次查找,将当前词法环境设置为解析环境
if (typeof LexicalEnvironment === 'undefined') {
LexicalEnvironment = currentLexicalEnvironment
}
// 检查环境的环境记录中是否有此绑定
let isExist = LexicalEnvironment.EnviromentRecord.HasBinding(name)
// 若是有则返回绑定值,没有则去外层环境查找
if (isExist) {
return LexicalEnvironment.EnviromentRecord[name]
} else {
return ResolveBinding(name, LexicalEnvironment.outer)
}
}
复制代码
上面讲了那么多理论知识,如今咱们结合代码来复习,有如下全局代码:
var x = 10
let y = 20
const z = 30
class Person {}
function foo() {
var a = 10
}
foo()
复制代码
如今咱们有了一个全局词法环境和foo函数词法环境(如下内容均为抽象伪代码):
// 全局词法环境
GlobalEnvironment = {
outer: null, // 全局环境的外部环境引用为null
// 全局环境记录,抽象为一个声明式环境记录和一个对象式环境记录的封装
GlobalEnvironmentRecord: {
// 全局this绑定值指向全局对象,即ObjectEnvironmentRecord的binding object
[[GlobalThisValue]]: ObjectEnvironmentRecord[[BindingObject]],
// 声明式环境记录,全局除了函数和var,其余声明绑定于此
DeclarativeEnvironmentRecord: {
y: 20,
z: 30,
Person: <<class>>
},
// 对象式环境记录的,绑定对象为全局对象,故其中的绑定能够经过访问全局对象的属性来得到
ObjectEnvironmentRecord: {
// 全局函数声明和var声明
x: 10,
foo: <<function>>,
// 内置全局属性
isNaN: <<function>>,
isFinite: <<function>>,
parseInt: <<function>>,
parseFloat: <<function>>,
Array: <<construct function>>,
Object: <<construct function>>
// 其余内置全局属性不一一列举
}
}
}
// foo函数词法环境
fooFunctionEnviroment = {
outer: GlobalEnvironment, // 外部词法环境引用指向全局环境
FunctionEnvironmentRecord: {
[[ThisValue]]: GlobalEnvironment, // foo函数全局调用,故this绑定指向全局环境
// 其余函数代码内的绑定
a: 10
}
}
复制代码
因为全局环境记录是声明式环境记录和对象式环境记录的封装,因此全局标识符的解析与其余环境的标识符解析有所不一样,下面介绍全局标识符解析的步骤(伪代码):
function GetGlobalBingingValue(name) {
// 全局环境记录
let rec = Global Environment Record
// 全局环境记录的声明式环境记录
let DecRec = rec.DeclarativeRecord
// HasBinding用来检查环境记录上是否绑定给定标识符
if (DecRec.HasBinding(name) === true) {
return DecRec[name]
}
let ObjRec = rec.ObjectRecord
if (ObjRec.HasBinding(name) === true) {
return ObjRec[name]
}
throw ReferenceError(`${name} is not defined`)
}
复制代码
能够看到读取全局变量时,先检索声明式环境记录,再检索对象式环境记录。这样就会出现一些有趣的现象:
let
、const
、class
等声明的变量若是存在同名var
变量或同名函数声明,就会报错(以后的文章中会具体介绍)。可是若是咱们使用let
、const
、class
声明变量,而后直接经过给全局对象添加一个同名属性,则能够绕过此类报错。
此时全局环境记录的声明式环境记录和对象式环境记录内都有此标识符的绑定,可是咱们访问时因为先检索声明式环境记录,因此对象式环境记录内的绑定会被遮蔽,要想访问只能经过访问全局对象属性的方法访问。
准备将以前写的部分深刻ECMAScript文章重写,加深本身理解,使内容更有干货,目录结构也更合理。
欢迎前往阅读系列文章,若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励。
菜鸟一枚,若是有疑问或者发现错误,能够在相应的 issues 进行提问或勘误,与你们共同进步。