😁 作者简介:一名大四的学生,致力学习前端开发技术
⭐️个人主页:夜宵饽饽的主页
❔ 系列专栏:JavaScript进阶指南
👐学习格言:成功不是终点,失败也并非末日,最重要的是继续前进的勇气
🔥前言:
这里是关于作用域真正的面目,涉及到编译时,作用域的作用和承担的角色,还有我们在查找变量时运用的LHS和RHS查询的方法,希望可以帮助到大家,欢迎大家的补充和纠正
文章目录
-
- [第1章 作用域是什么](#第1章 作用域是什么)
-
- [1.1 编译原理](#1.1 编译原理)
- [1.2 理解作用域](#1.2 理解作用域)
-
- [1.2.1 先介绍一下图中的关键人物](#1.2.1 先介绍一下图中的关键人物)
- [1.2.1 再介绍一下关键的过程](#1.2.1 再介绍一下关键的过程)
- [1.3 作用域的嵌套](#1.3 作用域的嵌套)
- [1.4 异常](#1.4 异常)
第1章 作用域是什么
几乎所有编程语言最基本的功能之一,就是能够储存变量当中的值,并且能在之后对这个值进行访问或修改。事实上,正是这种储存和访问变量的值的能力将状态带给了程序。
若没有了状态这个概念,程序虽然也能够执行一些简单的任务,但它会受到高度限制,做不到非常有趣。
但是将变量引入程序会引起几个很有意思的问题,也正是我们将要讨论的:这些变量住在哪里?换句话说,它们储存在哪里?最重要的是,程序需要时如何找到它们?
这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量。这套规则被称为作用域。
但是,究竟在哪里而且怎样设置这些作用域的规则呢?
1.1 编译原理
程序中的源代码在执行之前会经历三个步骤,统称为"编译"
-
分词/词法分析
这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序 var a = 2;。这段程序通常会被分解成为下面这些词法单元:var、a、=、2 、;。空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。
分词(tokenizing)和词法分析(Lexing)之间的区别是非常微妙、晦涩的,主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的
❓ 什么是有状态和无状态:
- 有状态:在有状态的识别中,解析器会维护一个状态,它会跟踪源代码的位置并根据当前状态识别词法单元。这意味着解析器会考虑上下文以确定如何解释源代码
- 无状态:在无状态的识别中,解析器不维护状态,它单纯地从源代码的开头开始逐个识别词法单元,而不考虑上下文
-
解析/语法分析
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为"抽象语法树"(Abstract Syntax Tree,AST)
-
代码生成
将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息息相关。抛开具体细节,简单来说就是有某种方法可以将 var a = 2; 的 AST 转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。
⭐️ 总结:
对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。在我们所要讨论的作用域背后,JavaScript 引擎用尽了各种办法(比如 JIT,可以延迟编译甚至实施重编译)来保证性能最佳。
1.2 理解作用域
1.2.1 先介绍一下图中的关键人物
- 引擎:从头到尾负责整个JavaScript程序的编译及执行过程
- 编译器:引擎的好朋友之一,负责语法分析及代码生成的脏话累活
- 作用域:引擎的另一位好朋友,负责收集并维护所有声明的标识符,组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些表示符的访问权限
1.2.1 再介绍一下关键的过程
- LHS查询:引擎对变量查询的一种方法,LHS 查询是赋值操作的左侧查询 。它发生在试图将一个值赋给一个变量时,例如:当你执行
var a = 42;
,JavaScript引擎需要进行LHS查询来找到变量a
,以便将值42
存储在变量a
中。 - RHS查询:RHS查询是赋值操作的右侧查询。它发生在试图获取变量的值时 ,而不是赋值,例如:如果你执行
console.log(a);
,JavaScript引擎会执行RHS查询来获取变量a
的值,并将其传递给console.log
函数进行打印。
1.3 作用域的嵌套
我们说过,作用域是根据名称查找变量的一套规则。实际情况中,通常需要同时顾及几个作用域。
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止
遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
我们这里可以引用《不知道的javaScript》的书籍中嵌套图:
这个建筑代表程序中的嵌套作用域链。第一层楼代表当前的执行作用域,也就是你所处的
位置。建筑的顶层代表全局作用域。
LHS 和 RHS 引用都会在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼,
如果还是没有找到就继续向上,以此类推。一旦抵达顶层(全局作用域),可能找到了你
所需的变量,也可能没找到,但无论如何查找过程都将停止。
1.4 异常
为什么区分LHS和RHS是一件重要的事情?
考虑以下代码:
js
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );
第一次对 b 进行 RHS 查询时是无法找到该变量的。也就是说,这是一个"未声明"的变量,因为在任何相关的作用域中都无法找到它。
-
如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError异常。值得注意的是,ReferenceError 是非常重要的异常类型。
-
相较之下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非"严格模式"下。
ES5 中引入了"严格模式"。同正常模式,或者说宽松 / 懒惰模式相比,严格模式在行为上有很多不同。其中一个不同的行为是严格模式禁止自动或隐式地创建全局变量。因此,在严格模式中 LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出同 RHS 查询失败时类似的ReferenceError异常