作用域是什么
1.1 编译原理
在传统编译语言
的流程中, 程序中的一段源代码在执行之前会经历三个步骤
, 统称为“编
译”
一、分词/词法分析
这个过程会将由字符组成的字符串分解成有意义的代码块
, 这些代码块被称为词法单元
。 例如, 考虑程序 var a = 2;。 这段程序通常会被分解成为下面这些词法单元: var、 a、 =、 2 、 ;
二、解析/语法分析
这个过程是将词法单元流(数组)
转换成一个由元素逐级嵌套
所组成的代表了程序语法
结构的树。 这个树被称为“抽象语法树
”(AST)。var a = 2; 的抽象语法树中可能会有一个叫作 VariableDeclaration 的顶级节点, 接下来是一个叫作 Identifier(它的值是 a) 的子节点, 以及一个叫作 AssignmentExpression的子节点。 AssignmentExpression 节点有一个叫作 NumericLiteral( 它的值是 2) 的子节点
三、代码生成
将 AST
转换为可执行代码
的过程称被称为代码生成
简单来说就是有某种方法可以将 var a = 2; 的 AST 转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中
比起那些编译过程只有三个步骤的语言的编译器, JavaScript 引擎要复杂得多
首先,JavaScript 的编译过程不是发生在构建之前
的,对于 JavaScript 来说, 大部分情况下编译发生在代码执行前的几微秒
(甚至更短!) 的时间内
简单地说, 任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)
。 因此,JavaScript 编译器首先会对 var a = 2; 这段程序进行编译, 然后做好执行它的准备, 并且通常马上就会执行它
1.2 理解作用域
一、演员表
首先介绍将参与到程序 var a = 2; 进行处理的过程中的演员们
-
引擎
从头到尾负责整个JavaScript 程序的编译及执行过程
-
编译器
引擎的朋友,负责语法分析及代码生成
等脏活累活 -
作用域
引擎的朋友,负责收集并维护由所有声明的标识符(变量) 组成的一系列查询
, 并实施
一套非常严格的规则
,确定
当前执行的代码对
这些标识符
的访问权限
二、对话
当你看见 var a = 2;
这段程序,可能认为这是一句声明,但引擎
却认为这里有两个完全不同的声明
,【一个由编译器在编译时处理,另一个则由引擎在运行时处理】。
下面将 var a = 2;
分解,看看引擎和它的朋友如何协同工作
编译器首先将 var a = 2;
分解为词法单元,然后将词法单元解析为一个树结构。但是当编译器进行代码生成时
,它对这段程序的处理方式与预期有所不同
(我们可能会认为:为一个变量分配内存,将其命名为a,然后将值2保存进这个变量,但这并不完全正确
)
但实际上,编译器会这样处理👇:
- 遇到
var a
, 编译器会询问作用域
是否已经有一个该名称的变量存在于同一个作用域的集合中。 如果是
, 编译器会忽略该声明, 继续进行编译;否
则它会要求作用域在当前作用域的集合中声明一个新的变量, 并命名为 a - 接下来编译器会
为引擎生成运行时所需的代码
, 这些代码被用来处理
a = 2 这个赋值操作
。 引擎运行时会首先询问作用域
, 在当前的作用域集合中是否存在
一个叫作 a 的变量。 如果是
, 引擎就会使用
这个变量; 如果否
, 引擎会继续查找
该变量
如果引擎最终找到
了 a 变量,就会将 2 赋值
给它。否则
引擎就会抛出一个异常
总结:
变量的赋值操作
会执行两个
动作, 首先编译器会在当前作用域中声明一个变量(如果之前没有声明过), 然后在运行时引擎会在作用域中查找该变量, 如果能够找到就会对它赋值
三、编译器有话说
编译器在编译过程中生成了代码后,引擎执行代码时,会通过查找
变量 a 来判断它是否已声明过。查找的过程中由作用域进行协助,查找的过程会影响结果。
在var a = 2;
,引擎会为变量 a 进行 LHS查询
。另外一个查找的类型叫做RHS
LHS 和 RHS 的含义是“赋值操作的左侧或右侧” 并不一定意味着就是“=赋值操作符的左侧或右侧”。在概念上最将其理解为“
赋值操作的目标是谁(LHS)
” 以及“谁是赋值操作的源头(RHS)
”
当变量出现在赋值操作的左侧时进行 LHS 查询, 出现在右侧时进行 RHS 查询,RHS查询与简单查找某个变量的值一样,而LHS查询则试图找到变量的容器本身,从而可以对其赋值。
RHS
理解成 retrieve his source value(取到它的源值
), 这意味着“得到某某的值
”
考虑下面代码:
console.log(a)
这里对a的引用是一个RHS引用,因为a没有赋予任何值,而需要查找并取得a的值
,这样才能将值传递给console.log(…)
考虑下面代码,既有LHS、也有RHS引用:
function foo(a) {
console.log(a); // 2
}
foo (2);
foo(..) 函数的调用
需要对 foo 进行 RHS
引用, 意味着“去找到 foo 的值
, 并把它给我”。 并且 (..) 意味着 foo 的值需要被执行
代码中有隐式的 a = 2
,此操作发生在 2 被当作参数传递给 foo(…) 函数时,2会被分配给参数 a 。为了给参数 a (隐式地)分配值,需要进行一次LHS查询
。
这里还有对 a 进行的 RHS 引用, 并且将得到的值传给了 console.log(…)。 console.log(…) 本身也需要一个引用才能执行, 因此会对 console 对象进行 RHS 查询
, 并且检查得到的值中是否有一个叫作 log 的方法
四、作用域嵌套
作用域是根据名称查找变量的一套规则
👉当一个块或函数嵌套在另一个块或函数中时, 就发生了作用域的嵌套。 因此, 在当前作用域中无法找到某个变量时,
引擎就会在外层嵌套的作用域中继续查找
, 直到找到该变量,或抵达最外层的作用域(也就是全局作用域) 为止
考虑下面代码:
function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4
回顾引擎和作用域之间的对话:
总结:
遍历嵌套作用域链的规则很简单: 引擎从当前的执行作用域开始查找变量, 如果找不到,就向上一级继续查找。 当抵达最外层的全局作用域时, 无论找到还是没找到, 查找过程都会停止
把作用域链比喻成一个建筑
这个建筑代表程序中的嵌套作用域链。 第一层楼代表当前的执行作用域, 也就是你所处的
位置。 建筑的顶层代表全局作用域
LHS 和 RHS 引用都会在当前楼层进行查找, 如果没有找到, 就会坐电梯前往上一层楼,
如果还是没有找到就继续向上, 以此类推。 一旦抵达顶层(全局作用域), 可能找到了你所需的变量, 也可能没找到, 但无论如何查找过程都将停止
总结
作用域是一套规则, 用于确定在何处以及如何查找变量(标识符)。 如果查找的目的是对变量进行赋值, 那么就会使用 LHS 查询; 如果目的是获取变量的值, 就会使用 RHS 查询
赋值操作符会导致 LHS 查询。 = 操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作
JavaScript 引擎首先会在代码执行前对其进行编译, 在这个过程中, 像 var a = 2 这样的声明会被分解成两个独立的步骤:
- 首先, var a 在其作用域中声明新变量。 这会在最开始的阶段, 也就是代码执行前进行
- 接下来, a = 2 会查询(LHS 查询) 变量 a 并对其进行赋值
LHS 和 RHS 查询都会在当前执行作用域中开始, 如果有需要(也就是说它们没有找到所
需的标识符), 就会向上级作用域继续查找目标标识符, 这样每次上升一级作用域(一层
楼), 最后抵达全局作用域(顶层), 无论找到或没找到都将停止不成功的 RHS 引用会导致抛出 ReferenceError 异常。 不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下), 该变量使用 LHS 引用的目标作为标识符, 或者抛出 ReferenceError 异常(严格模式下)