概要
理清执行上下文、作用域、环境记录、词法环境、变量环境这些概念的联系。
执行上下文
主要分为三类:
- 当js执行全局代码时,会编译全局代码并创建 全局执行上下文,全局执行上下文只会创建一个
- 当调用一个函数时,函数体内的代码会被编译,并创建函数执行上下文,一般情况,函数执行结束后,刚刚所创建的执行上下文 会被推出 js调用栈并销毁
- 当使用eval函数时,eval的代码也会被编译,并创建执行上下文
这边重点关注下全局上下文和函数上下文。
全局执行上下文会创建全局环境记录,而函数执行上下文会创建时会同时创建This Binding、VariableEnvironment(变量环境组件)、LexicalEnvironment(词法环境组件)。全局环境记录、VariableEnvironment(变量环境组件)和LexicalEnvironment(词法环境组件)都是一种环境记录。
什么是环境记录
根据最新tc39规范文档看https://tc39.es/ecma262/#sec-environment-records
- 环境记录是一种规范类型,用于记录代码中的标识符与变量的映射,类似一个对象或者map。
- 环境记录分为 声明式环境记录DeclarativeEnvironmentRecord、对象环境记录 ObjectEnvironmentRecord 和 全局环境记录 GlobalEnvironmentRecord
- 每个环境记录都有一个outerEnv 字段,outerEnv可能为null(全局环境记录的outerEnv为null) 或者 是对外部环境记录的引用(就是由这个字段 形成的作用域链)
什么是声明式环境记录
- 声明式环境记录 绑定了由包含在其范围内的声明定义的标识符集,即var、let、const、class、module、import 和function函数声明
- 声明式环境记录 又分为 函数环境记录FunctionEnvironmentRecords 和 模块环境记录Module Environment Records
- 函数环境记录 是声明式环境记录 用于表示函数的顶级范围,有Arguments对象,并且如果函数不是箭头函数,会提供this绑定
看一下全局环境记录长啥样
根据规范文档https://tc39.es/ecma262/#sec-global-environment-records,可以知道:
- 全局环境记录 是声明式环境记录 和 对象式环境记录的组合
- 对象式环境记录主要是存在全局环境记录 和 with创建的环境记录
那伪代码大致长这样:
GlobalEnvironmentRecords: {
outerEnv: null, // 全局环境 的外部引用为null
[[GlobalThisValue]]: // this的执行 如 window
[[ObjectRecord]]: { // 即对象式环境记录ObjectEnvironmentRecord
// 包含了全局下var、function、generator、async声明的标识符 还有其他内置对象 如Math、Date
// 用全局对象(如window)作为绑定对象,所以在全局下用var、function...声明的变量可以通过window[变量名] 访问(或window.变量名)
[变量名]: undefined
},
[[DeclarativeRecord]]:{ // 即声明式环境记录DeclarativeEnvironmentRecord
// 除了var、function、generator、async声明的标识符保存在这里,如let、const
[变量名]: uninitialized // 在编译阶段为uninitialized
},
[[varNames]]: // var、function、generator、async声明的标识符列表
}
注:并不是说声明式环境记录只能存放let、const,只是在全局环境记录下的 表现如此。
函数执行上下则会创建 词法环境LexicalEnvironment 和 可变环境VariableEnvironment这两个环境记录组件,并且都属于声明式环境记录。
LexicalEnvironment是标识用于解析此执行上下文中的代码所做的标识符引用的环境记录。 VariableEnvironment是标识保存此执行上下文中由 VariableStatements 创建的绑定的环境记录。
可以简单理解为VariableEnvironment 保存着var和函数声明的标识符,LexicalEnvironment则保存着let、const声明的标识符。
伪代码:
FunctionExecutionContext = {
VariableEnvironment: {
this: ...
// 保存var声明的标识符、function
outerEnv: GlobalExecutionContext // 外部环境记录
},
LexicalEnvironment: {
// 保存let、const声明的标识符
outerEnv: GlobalExecutionContext // 外部环境记录
this: ...
argument: {...}
}
}
那作用域和作用域链又是啥?
- 借用《你不知道的js》里的定义:我们将作用域定义为一套规则,这套规则用来管理js引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。其实现在的作用域也可以简单理解为就是环境记录
- 我们知道变量查找即是 查找到环境记录里查找,如果查找不到,则会寻找当前环境记录的outerEnv 所指向的外部环境记录里查找,一直到最顶层的全局环境记录,因此环境记录的outerEnv所在向的链表形成了作用域链
- 而作用域又分为 词法作用域(即静态作用域) 和 动态作用域
词法作用域
- JS 的作用域遵循的就是词法作用域模型,即静态作用域
- 词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,可以理解为变量查找的环境记录以及外部环境记录在代码书写完成时就已经决定了
var str = 'global'
function printStr() {
console.log(str)
}
function test() {
var str = 'text'
printStr() // 打印 global,因为作用域由 函数声明的位置决定,所以consoleStr函数引用的外部环境是 全局环境,所以当前函数的词法环境没有str,就会查找外部环境记录,于是打印 global
function innerPrint() {
console.log(str)
}
innerPrint() // innerPrint的外部环境 指向 test函数的环境记录,所以打印 test
}
test()
因为test函数是在全局环境声明的,所以test函数里的环境记录的outerEnv指向的是全局环境记录,因此打印global。
而innerPrint函数则是在test函数内部声明,所以outerEnv指向的是test函数内部的环境记录。
总结
- 环境记录是用于记录代码中变量和函数标识符的映射
- 执行上下文分为全局执行上下文、函数执行上下文和eval创建的执行上下文
- 全局上下文会创建全局环境记录,函数执行上下会创建词法环境组件和变量环境组件
- 词法环境组件用于保存let、const标识符,变量环境组件用于保存var和函数声明的标识符
- 作用域是变量查找的规则,可以简单理解作用域就是环境记录,环境记录的outerEnv所在的链表形成了作用域链
- js是采用词法(静态)作用域模型,即环境记录以及所指向的外部环境记录由你代码书写的位置决定
PS: 文中我喜欢用可以理解一词,意思是因为这样理解并不是百分百正确(毕竟我也不是专家。。害),只是为了便于自身理解和记忆,只要代码执行的结果没有错,中间过程大致方向没有错误,那当然是自己觉得怎么容易理解怎么来,好了喝奶茶去了~