前言

在说一个概念前,咱们须要确定它的前提,此文以 ECMAScript5 为根底撰写

一句话解释

词法环境就是在 JavaScript 代码编译阶段记录变量申明、函数申明、函数申明的形参的合集

JavaScript 的编译过程

在介绍词法环境前,咱们先看下在 V8 里 JavaScript 的编译执行过程,大抵分为三个阶段

第一步:V8 引擎刚拿到 执行上下文 的时候,会把代码从上到下一行一行的先做分词/词法剖析(Tokenizing/Lexing)。分词是指:比方 var a = 2; 这段代码,会被分词为:var a 2;这样的原子符号(atomic token);词法剖析是指:注销变量申明、函数申明、函数申明的形参

第二步:在分词完结当前,会做代码解析,引擎将 token 解析翻译成一个 AST(形象语法树), 在这一步的时候,如果发现语法错误,就会间接报错不会再往下执行

第三步:引擎生成 CPU 能够执行的机器码

在第一步里有个词法剖析,它用来注销变量申明、函数申明、函数申明的形参,后续代码执行的时候就晓得去哪里拿变量的值和函数了,这个注销的中央就是Lexical Environment(词法环境)

——深刻了解 JavaScript-词法环境

总结一下:引擎会在解释 JavaScript 代码之前首先对其进行编译。编译器的一部分工作就是找到所有的申明,并用适合的作用域将它们关联起来

咱们先升到一万米低空,看一下整个 JavaScript 的执行生命周期

JavaScript 的执行生命周期分成两个阶段,编译阶段执行阶段

  • 编译阶段由编译器实现,它将代码翻译成可执行代码,这个阶段能晓得全副标识符在哪里、如何申明的以及作用域规定

    • 编译阶段进行变量申明
    • 编译阶段变量申明进行晋升,然而指为 undefined
    • 编译阶段所有非表达式的函数申明进行晋升
  • 代码执行阶段即执行可运行代码,生成执行上下文,这部分由引擎实现

    • 负责 变量赋值函数援用 以及执行代码

(PS:对 JavaScript 而言,大部分状况下编译产生在代码执行前的几微秒)

咱们要说的 词法环境 就是在编译阶段负责收集的”容器“

留神:JavaScript 采纳的是词法作用域(动态作用域),所以词法环境是与咱们所写的代码构造绝对应,换句话说,咱们将代码写成什么样,词法环境就是怎么样子。词法环境是在代码定义的时候决定的,跟代码在哪里调用没有关系。

词法环境由什么组成

词法环境的外部有两局部组成:环境记录器(Environment Record)对外部环境的援用(outer)

  1. 环境记录器记录存储变量、函数申明以及函数申明的形参
  2. 外部环境的援用意味着它能够拜访其父级词法环境(作用域)

环境记录器又分为两种

  1. 申明式环境记录(Declarative Environment Record):用来记录间接有标识符定义的元素,比方变量、常量、let、class、module、import 以及函数申明。
  2. 对象式环境记录(Object Environment Record):次要用于 with 和 global 的词法环境。

其中 申明式环境记录(Declarative Environment Record),又分为两种类型:

  • 函数环境记录(Function Environment Record):用于函数作用域。
  • 模块环境记录(Module Environment Record):模块环境记录用于体现一个模块的内部作用域,即模块 export 所在环境。

咱们做一个分类图,更加具象地意识词法环境所蕴含的货色

环境记录器很好了解,无非就是变量汇合,那什么是 outer 呢

在之前介绍 作用域 的文章中咱们已经总结过:JavaScript 的作用域是词法作用域,它由函数在那里定义无关

而 outer 就是指向词法环境的父级词法环境(作用域)

咱们举个例子来看一下词法环境的形成元素:

var a = 1;function foo() {    console.log(a);    function bar() {        var b = 2;        console.log(a * b);    }    bar();}function baz() {    var a = 10;    foo();}baz();

它的词法作用域关系图如下:

更加具象的关系图如下:

咱们也能够用伪代码来模仿下面代码的词法环境:

// 全局词法环境GlobalEnvironment = {    outer: null, // 全局环境的外部环境援用为null    GlobalEnvironmentRecord: {        // 全局 this 绑定指向全局对象        [[GlobalThisValue]]: ObjectEnvironmentRecord[[BindingObject]],        // 申明式环境记录,除了全局函数和 var ,其余申明都绑定在这里        DeclarativeEnvironmentRecord: {},        // 对象式环境记录,绑定对象为全局对象        ObjectEnvironmentRecord: {            a: 1,            foo: << function >>,            baz: << function >>,            isNaN: << function >>,            isFinite: << function>>,            parseInt: << function>>,            parseFloat: << function>>,            Array: << construct function>>,            Object: << construct function>>,            ...        }    }}//foo 函数的词法环境fooFunctionEnvironment = {    outer: GlobalEnvironment, // 内部词法环境援用全局环境    FunctionEnvironmentRecord: {        [[ThisValue]]: GlobalEnviroment, // this绑定指向全局环境        bar: << function >>    }}// bar 函数的词法环境barFunctionEnvironment = {    outer: fooFunctionEnviroment, // 内部词法环境援用foo函数词法环境    FunctionEnvironmentRecord: {        [[ThisValue]]: GlobalEnviroment, // this绑定指向全局环境          b: 2    }}// baz 函数的词法环境bazFunctionEnvironment = {    outer: GlobalEviroment, // 内部词法环境援用指向全局环境    FuntionEnvironmentRecord: {        [[ThisValue]]: GlobalEnviroment, // this绑定指向全局环境        a: 10    }}

咱们能够看出词法环境的两个重要组成部分,其中 outer 由作用域决定,环境记录器记录所有的变量,当在本词法环境中找不到变量时,就会引着 outer 往父级词法环境中找变量,这就造成了作用域链

变量晋升及函数晋升

就像咱们之前所说,在编译阶段,包含变量和函数在内的所有申明都会在任何代码被执行前首先解决

当你看到 var a = 1; 时,可能会认为这是一个申明。但 JavaScript 实际上会将其看成两个意思:var a = undefined;a = 2; 。第一个定义申明在编译阶段进行,第二个赋值申明会被留在原地期待执行阶段

举个例子:

var a = 1;var b = true;function foo() {    console.log(a);}foo();

在代码执行之前,即编译阶段:

a = undefined;b = undefined;foo = function () {    console.log(a);};

执行阶段:

a = 1;b = true;foo = function () {    console.log(a);};

函数优先

函数申明和变量申明都会被晋升。然而这个值得注意的细节是函数的优先级大于变量

例如上面的代码:

foo();var foo;function foo() {    console.log(1);}foo = function () {    console.log(2);};

<details>

<summary>答案</summary>输入 1 而不是 undefined 或者 2

</details>

这段代码会被引起了解为如下模式:

function foo() {    console.log(1);}// var foo 被疏忽foo(); // 1foo = function () {    console.log(2);};

留神,var foo 只管呈现在 function foo() ... 的申明之前,但函数申明的优先级大于变量晋升,即便它写在函数后面,然而还是会以函数为根据展现(变量被疏忽)

foo();function foo() {    console.log(1);}var foo = function () {    console.log(2);};function foo() {    console.log(3);}

<details>

<summary>答案</summary>输入 3

</details>

说到函数申明和变量申明,咱们能够举出很多例子,例如这个例子

function bar() {    console.log('bar1');}var bar = function () {    console.log('bar2');};bar();

<details>

<summary>答案</summary>bar2

</details>

调换程序呢:

var bar = function () {    console.log('bar2');};function bar() {    console.log('bar1');}bar();

<details>

<summary>答案</summary>bar2

</details>

实质上这些题目绕不开之前俺们说的原理:编译阶段进行函数、变量晋升,执行阶段在原处执行代码。在编译阶段函数 bar 晋升,执行阶段,bar 赋值给 function() {...},输入后果 bar2

var、let、const、function 等都会被晋升(hoist),只是 let、const 不会被初始化,所以提前应用会报 ReferenceError

总结

咱们介绍了词法环境,从它是怎么产生,到它是什么(由什么组成),再到前面的函数、变量晋升

理解词法环境为是咱们下一节—— 执行上下文与调用栈(后续文章更新)打下了根底

参考资料

  • 了解 JavaScript 中的执行上下文和执行栈
  • JS:深刻了解 JavaScript-词法环境
  • 书:你不晓得的 JavaScript(上卷)

系列文章

  • 深刻了解JavaScript——开篇
  • 深刻了解JavaScript——JavaScript 是什么
  • 深刻了解JavaScript——JavaScript 由什么组成
  • 深刻了解JavaScript——所有皆对象
  • 深刻了解JavaScript——Object(对象)
  • 深刻了解JavaScript——new 做了什么
  • 深刻了解JavaScript——Object.create
  • 深刻了解JavaScript——拷贝的机密
  • 深刻了解JavaScript——原型
  • 深刻了解JavaScript——继承
  • 深刻了解JavaScript——JavaScript 中的始皇
  • 深刻了解JavaScript——instanceof——找祖籍
  • 深刻了解JavaScript——Function
  • 深刻了解JavaScript——作用域
  • 深刻了解JavaScript——this关键字
  • 深刻了解JavaScript——call、apply、bind三大将
  • 深刻了解JavaScript——立刻执行函数(IIFE)