深入理解JavaScript-词法环境

简介: 深入理解JavaScript-词法环境

前言


在说一个概念前,我们需要确定它的前提,此文以 ECMAScript5 为基础撰写


一句话解释


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


JavaScript 的编译过程



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


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

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

第三步:引擎生成 CPU 可以执行的机器码

在第一步里有个词法分析,它用来登记变量声明、函数声明、函数声明的形参,后续代码执行的时候就知道去哪里拿变量的值和函数了,这个登记的地方就是Lexical Environment(词法环境)

——深入理解 JavaScript-词法环境[1]


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

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

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


  • 编译阶段由编译器完成,它将代码翻译成可执行代码,这个阶段能知道全部标识符在哪里、如何声明的以及作用域规则
  • 编译阶段进行变量声明
  • 编译阶段变量声明进行提升,但是指为 undefined
  • 编译阶段所有非表达式的函数声明进行提升
  • 代码执行阶段即执行可运行代码,生成执行上下文,这部分由引擎完成
  • 负责 变量赋值函数引用 以及执行代码

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

image.png

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

注意: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 所在环境。

我们做一个分类图,更加具象地认识词法环境所包含的东西

image.png

环境记录器很好理解,无非就是变量集合,那什么是 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();


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

image.png

更加具象的关系图如下:

image.png

我们也可以用伪代码来模拟上面代码的词法环境:

// 全局词法环境
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);
};


答案输出 1 而不是 undefined 或者 2

这段代码会被引起理解为如下形式:

function foo() {
    console.log(1);
}
// var foo 被忽略
foo(); // 1
foo = 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);
}

答案输出 3


说到函数声明和变量声明,我们可以举出很多例子,例如这个例子

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

答案bar2


调换顺序呢:

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

答案bar2


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


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


总结


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

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


参考资料


  • 理解 JavaScript 中的执行上下文和执行栈[2]
  • JS:深入理解 JavaScript-词法环境[3]
  • 书:你不知道的 JavaScript(上卷)


[1] 深入理解 JavaScript-词法环境: https://limeii.github.io/2019/05/js-lexical-environment/

[2] 理解 JavaScript 中的执行上下文和执行栈: https://github.com/xitu/gold-miner/blob/master/TODO1/understanding-execution-context-and-execution-stack-in-javascript.md

[3] JS:深入理解 JavaScript-词法环境: https://limeii.github.io/2019/05/js-lexical-environment/

相关文章
|
12天前
|
Web App开发 JavaScript 前端开发
如何确保 Math 对象的方法在不同的 JavaScript 环境中具有一致的精度?
【10月更文挑战第29天】通过遵循标准和最佳实践、采用固定精度计算、进行全面的测试与验证、避免隐式类型转换以及持续关注和更新等方法,可以在很大程度上确保Math对象的方法在不同的JavaScript环境中具有一致的精度,从而提高代码的可靠性和可移植性。
|
12天前
|
自然语言处理 JavaScript 前端开发
[JS]作用域的“生产者”——词法作用域
本文介绍了JavaScript中的作用域模型与作用域,包括词法作用域和动态作用域的区别,以及全局作用域、函数作用域和块级作用域的特点。通过具体示例详细解析了变量提升、块级作用域中的暂时性死区等问题,并探讨了如何在循环中使用`var`和`let`的不同效果。最后,介绍了两种可以“欺骗”词法作用域的方法:`eval(str)`和`with(obj)`。文章结合了多位博主的总结,帮助读者更快速、便捷地掌握这些知识点。
25 2
[JS]作用域的“生产者”——词法作用域
|
11天前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
|
17天前
|
Web App开发 JavaScript 前端开发
探索Deno:新一代JavaScript/TypeScript运行时环境
【10月更文挑战第25天】Deno 是一个新兴的 JavaScript/TypeScript 运行时环境,由 Node.js 创始人 Ryan Dahl 发起。本文介绍了 Deno 的核心特性,如安全性、现代化、性能和 TypeScript 支持,以及开发技巧和实用工具。Deno 通过解决 Node.js 的设计问题,提供了更好的开发体验,未来有望进一步集成 WebAssembly,拓展其生态系统。
|
6月前
|
前端开发 测试技术
测Nuxt.js入坑,配置dev、test、pro三种环境的变量env
先下载一个cross-env模块,比较好控制环境
213 5
|
2月前
|
SQL JavaScript 数据库
sqlite在Windows环境下安装、使用、node.js连接
sqlite在Windows环境下安装、使用、node.js连接
|
3月前
|
编解码 JavaScript 前端开发
JS逆向浏览器脱环境专题:事件学习和编写、DOM和BOM结构、指纹验证排查、代理自吐环境通杀环境检测、脱环境框架、脱环境插件解决
JS逆向浏览器脱环境专题:事件学习和编写、DOM和BOM结构、指纹验证排查、代理自吐环境通杀环境检测、脱环境框架、脱环境插件解决
107 1
|
3月前
|
自然语言处理 JavaScript 前端开发
JS自学——快速了解词法作用域及欺骗词法作用域
JS自学——快速了解词法作用域及欺骗词法作用域
|
3月前
|
缓存 JavaScript Ubuntu
Node.js环境怎么搭建?
【8月更文挑战第4天】Node.js环境怎么搭建?
66 1
|
3月前
|
JavaScript Serverless Linux
函数计算产品使用问题之遇到Node.js环境下的请求日志没有正常输出时,该如何排查
函数计算产品作为一种事件驱动的全托管计算服务,让用户能够专注于业务逻辑的编写,而无需关心底层服务器的管理与运维。你可以有效地利用函数计算产品来支撑各类应用场景,从简单的数据处理到复杂的业务逻辑,实现快速、高效、低成本的云上部署与运维。以下是一些关于使用函数计算产品的合集和要点,帮助你更好地理解和应用这一服务。