我们在看一些讲解 JavaScript 基础和进阶博客中,或者视频中,会经常听到一个名词——词法环境。那究竟什么是词法环境?它的作用是什么?今天我们就来唠叨唠叨。
首先来看看词法环境的概念。
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.
翻译:词法环境是一种规范类型,用于根据ECMAScript代码的词法嵌套结构定义标识符与特定变量和函数的关联。词法环境由环境记录和对外部词法环境的可能空引用组成。
这是 ES2020 对词法环境的解释,可能刚开始看会一脸懵,不过不要紧,我们可以这样去理解它。
词法环境
词法环境是由内部 JavaScript 引擎构造,用来保存标识符变量的映射关系。同时,它还保存对父级词法环境的引用。
那么这里的标识符就是指变量或者函数的名称,变量是对实际对象(包括函数类型对象)或者原始值的引用。
每当 JavaScript 引擎创建执行上下文来执行函数或者全局代码时,就会创建一个新的词法环境,以存储在该函数执行期间在该函数中定义的变量。
词法环境有两个组成部分:
- 环境记录:存储变量和函数声明的实际位置
- 对外部环境的引用:实际上就是对外部或者说是父级词法环境的引用。这对理解闭包是如何工作的尤为重要。
我们可以用下面的例子来理解词法环境:
lexicalEnvironment = {
environmentRecord: { // 环境记录
<identifier>: <value>,
<identifier>: <value>
}
outer: <Reference to the parent lexical environment> // 外部环境引用
}
来看个实际例子:
const text = 'Hello Lexical Environment'
function fn() {
const fnText = 'Inside Lexical Environment'
console.log('Inside function')
}
fn();
console.log('Inside global execution context')
我们前面说到过,当 JavaScript 创建一个全局执行上下文来执行全局代码时,它还会创建一个新的词法环境用来存储全局范围内定义的变量和函数。所以对这个全局的词法环境就有:
globalLexicalEnvironment = {
environmentRecord: {
text: 'Hello Lexical Environment',
fn: < reference to function object >
}
outer: null
}
这是全局作用域下的词法环境,所以外层这里是 null。
而当 JavaScript 引擎为函数 fn 创建执行上下文时,它会再创建一个词法环境来存储在函数执行期间在该函数内部定义的变量。函数 fn 的词法环境如下:
functionLexicalEnvironment = {
environmentRecord: {
fnText: 'Inside Lexical Environment'
}
outer: < globalLexicalEnvironment >
}
这里就可以看到 outer 引用的是父级的词法环境。
当函数完成时,它的执行上下文将从堆栈中删除,但它的词法环境就不一定了,可能会从内存中删除,也可能不会从内存中删除,这取决于该词法环境是否被其外部词法环境属性中的任何其他词法环境引用。典型的例子就是闭包。
所以我们小小的总结一下:
词法环境就是在JavaScript 引擎创建一个执行上下文时,创建的用来存储变量和函数声明的环境,它可以使代码在执行期间,访问到存储在其内部的变量和函数,而在代码执行完毕之后,从内存中释放掉。
注意:通过 var 定义的变量,存在于变量环境。
我们来看一个简单的例子,看看变量环境、词法环境、执行上下文(this)和通过作用域查找某个变量这样一个过程:
var global_variable1 = 'Hello';
let global_variable2 = 'World';
function fn() {
var inside_variable1 = 'fn';
let inside_variable2 = 'function';
{
var block_variable1 = 'var_block';
let block_variable2 = 'let_block';
}
}
fn()
实际的代码执行顺序是:
// 全局代码的执行过程
var global_variable1;
fn;
global_variable1 = 'Hello';
let global_variable2 = 'World';
fn = function() {}
fn()
// fn 内部执行顺序
var inside_variable1;
var block_variable1;
inside_variable1 = 'fn';
let inside_variable2 = 'function';
{
block_variable1 = 'var_block';
let block_variable2 = 'let_block';
}
代码在被 JavaScript 引擎编译并创建全局上下文,放入到调用栈中,如下图:
当全局代码执行到函数 fn 的时候,创建函数 fn 的执行上下文,放入到调用栈当中:
而当代码执行到块级作用域时,块级作用域不存在编译过程,词法环境就是一个小型的栈,所以这里块级作用域的执行是将变量加入到当前 fn 环境的词法环境中去。(就懒得画图了,自行脑补)。
我们再来看看作用域链的查找是怎样的,假如我们现在要查找全局变量 global_variable1,那么会从 fn 的词法环境向上逐一查找,直到找到为止。
fn 词法环境--》fn 变量环境--》全局词法环境--》全局变量环境
最后在代码执行完之后,挨个儿出栈。