《用得上的前端知识》系列 - 你我都很忙,能用100字说清楚,绝不写万字长文
基本概念
词法环境的构成
词法环境有以下两个组成部分:
- 环境记录(Environment Record):记录相应代码块内所有标识符的绑定,如:变量声明、函数声明(形参)等都储存于此;
- 对外部词法环境的引用(outer):用于形成多个词法环境在逻辑上的嵌套结构,以实现可以访问外部词法环境变量的能力。
环境记录的类型有以下 2 种:
- 声明式环境记录(Declarative Environment Record);
- 对象式环境记录(Object Environment Record)。
-
- 以对象形式在代码中可见的“环境记录”,在 ES5.1 规范中称之为对象式环境记录;
- 每个对象式环境记录都与一个对象相关联,这个对象叫做对象式环境记录的binding object;
- with语句和全局词法环境的环境记录均为“对象式环境记录”。
注1:当可执行代码中含有“let、const、class”等关键字或执行诸如“函数、代码块、TryCatch中的Catch从句”这类代码时,都会创建新的词法环境;
注2:词法环境(Lexical Environment)和环境记录(Environment Record)都是一种规范类型(specification type)。规范类型可以理解为描述数据类型的类型;
注3:从 ES5 开始,就把作用域概念改成了词法环境概念;
综合案例1
function foo() {
var name = 'Calvin';
let country: 'China';
const age = 20;
console.log(name, age, country); // "Calvin 20 China"
var girl = {
name: 'Debby',
age: 18,
};
with (girl) {
console.log(name, age, country); // "Debby 18 China"
}
console.log(name, age, country); //"Calvin 20 China"
}
foo();
foo 函数的整个运行过程(以下展示的只是用于说明执行过程的伪代码):
//执行全局代码 --------------------------------
global_EC = { //全局执行上下文
LE : { //词法环境组件,可以理解为一个引用,指向一个词法环境对象
ER : window, //环境记录,是一个“对象式环境记录”
outer : null
},
VE : null, //变量环境组件
this : window //this绑定组件
}
global_EC.VE = global_EC.LE; //让变量环境组件和词法环境组件指向同一个词法环境对象
//进入 foo 函数 --------------------------------
foo_EC = {} //创建函数执行上下文
foo_EC.LE = { //创建与函数执行上下文相关的词法环境对象
ER : {
arguments : <Arguments(0)>,
name : undefined
},
outer : global_EC.LE
}
foo_EC.VE = foo_EC.LE; //让变量环境组件和词法环境组件指向同一个词法环境对象
foo_EC.LE = {
ER : {
country, //只实例化了标识符,并没有初始化,所以直接调用会报错
age //只实例化了标识符,并没有初始化,所以直接调用会报错
},
outer : foo_EC.VE
}
//执行至第一处 console.log 时 --------------------------------
foo_EC.VE = { //foo 函数对应的词法环境对象
ER : {
arguments : <Arguments(0)>,
name : 'Calvin'
},
outer : global_EC.LE
}
foo_EC.LE = {
ER : {
country : 'China',
age : 18
},
outer : foo_EC.VE
}
//进入 with 语句 --------------------------------
older_foo_LE = foo_EC.LE; //先把之前的词法环境对象保存起来
foo_EC.LE = { //然后替换成新创建的词法环境对象
ER : girl,
outer : older_foo_LE
}
//with 块代码执行完毕,执行至第三处 console.log 时,还原 foo 函数的词法环境对象 ----------
foo_EC.LE = older_foo_LE;
//foo执行完毕 调用栈弹出foo_EC并销毁 --------------------------------
foo_EC=null;
综合案例2
let a = '面包'
var b = '可乐';
if( true ){
const c = '汉堡';
console.log(c)
}
function helloFn(){
var d = '鸡翅';
const e = '中国凉茶'
console.log(e, d)
}
helloFn();
随着代码的执行,执行上下文的变化过程如下图所示:
首先,执行全局代码,创建“全局执行上下文”。通过 let 声明的变量放在“声明式环境记录”中,通过 var 声明的变量和函数声明被放在对象式环境记录中。
执行到(1)处时,执行赋值语句,变量被赋值,所以 a 的值为“面包”,b 的值为“可乐”。
执行到(2)处时,由于 if 语句中出现了 const 声明,因此会出现块级作用域。如上图,创建新的词法环境对象,用以处理块级作用域内的变量。由于 var 声明会出现变量提升,所以,每个执行上下文中,只有 1 个“对象式环境记录”。
词法环境对象之间通过 outer 进行关联。
说明:
- 根据规范,能产生执行上下文的只有 3 种可执行代码:全局代码、函数和eval;
- 根据规范,每次执行诸如 FunctionDeclaration,WithStatement 或 TryStatement的Catch 子句的时候,都会创建新的词法环境。
因此,块级作用域只会产生新的词法环境,不会产生新的执行上下文。
执行到(3)处时,helloFn 函数被执行,产生新的函数上下文。
注:变量查找的顺序为,在当前执行上下文的“声明式环境记录” -> 当前执行上下文的“对象式环境记录” -> 通过 outer 找到上一级词法环境,然后先查找“声明式环境记录”,再到“对象式环境记录”,以此类推。如下图所示:
参考资料
- juejin.cn/post/684490…
- cloud.tencent.com/developer/a…
- www.w3.org/html/ig/zh/… – ES5 中文文档
- 262.ecma-international.org/5.1/ – ES5 英文文档
- 262.ecma-international.org/6.0/#sec-le… – ES6 英文文档