词法环境

186 阅读4分钟

《用得上的前端知识》系列 - 你我都很忙,能用100字说清楚,绝不写万字长文

基本概念

  • 词法环境:是一个用于定义特定变量和函数标识符在 ECMAScript 代码的词法嵌套结构上关联关系的规范类型

词法环境的构成

词法环境有以下两个组成部分:

  • 环境记录(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();

随着代码的执行,执行上下文的变化过程如下图所示:

image.png 首先,执行全局代码,创建“全局执行上下文”。通过 let 声明的变量放在“声明式环境记录”中,通过 var 声明的变量和函数声明被放在对象式环境记录中。

image.png 执行到(1)处时,执行赋值语句,变量被赋值,所以 a 的值为“面包”,b 的值为“可乐”。

image.png 执行到(2)处时,由于 if 语句中出现了 const 声明,因此会出现块级作用域。如上图,创建新的词法环境对象,用以处理块级作用域内的变量。由于 var 声明会出现变量提升,所以,每个执行上下文中,只有 1 个“对象式环境记录”。

词法环境对象之间通过 outer 进行关联。

说明:

  • 根据规范,能产生执行上下文的只有 3 种可执行代码:全局代码、函数和eval;
  • 根据规范,每次执行诸如 FunctionDeclaration,WithStatement 或 TryStatement的Catch 子句的时候,都会创建新的词法环境。

因此,块级作用域只会产生新的词法环境,不会产生新的执行上下文。

image.png 执行到(3)处时,helloFn 函数被执行,产生新的函数上下文。

注:变量查找的顺序为,在当前执行上下文的“声明式环境记录” -> 当前执行上下文的“对象式环境记录” -> 通过 outer 找到上一级词法环境,然后先查找“声明式环境记录”,再到“对象式环境记录”,以此类推。如下图所示:

image.png

参考资料