持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 3 天,点击查看活动详情
javaScript 作用域——作用域是什么?
编译原理
大部分编程语言的基本功能之一,就是能够存储变量当做值,并且能对这个值进行修改和删除。正是这种存储和访问变量的值的能力将状态带给了程序。但是,我们不禁要问了,这些状态变量存储在哪里?他们是怎么样被找到的?
传统的编译流程一般分为三个阶段:
-
词法解析
这个过程会将由字符组成的字符串解析成有意义的代码块,这些代码块被称为词法单元,比如说:
var a = 1; var b = 2;会被解析成var, a, =,2,;,var,b,=,2,;这些词法单元。至于空格是否会被解析,则看空格在这门编程语言当中是否有特殊意义。 -
语法解析
词法解析完成后,会生成一个数组,也就是
词法单元流,这个数组会被转换成一个有元素逐级嵌套组成的树形结构,而这个树形结构被称为抽象语法树。var a = 2;解析成抽象语法树,会有一个顶级节点VarableDeclaration,接下来是一个叫作Identifier(它的值是 2)的子节点,以及一个叫作AssignmentExpression的子节点。AssignmentExpression节点有一个叫作NumericLiteral(它的子也为 2)的子节点。 -
代码生成
代码生成就是将抽象语法树转换为可执行代码的过程。比如将var a = 1;转换成可执行代码简单来讲就是创建了一个变量a,并且为a赋值为 1。
在javascript当中的编译比上面的传统编译还要复杂。在 javascript 进行编译的时候,在语法分析和代码生成阶段,需要对运行的性能进行优化,以提高编译的效率。但是很显然,浏览器不会给我们这么多时间来对代码进行优化。对于 javascript 来说,大部分情况下编译发生时间再代码执行的前几毫秒。在这短短的时间里面,编译器会尽可能的对代码进行优化。
简单来说,javascript 在执行代码前会对其进行编译,编译的时机就在执行前的毫秒,甚至更短。
理解作用域
想要理解作用域,我们首先要知道代码在执行的过程当中是如何使用作用域的,在 javascript 当中,编译器、作用域、引擎三者之间构成了一个很大的关系。
比如:var a = 2;这句代码,编译器解析,引擎执行的过程会发生什么呢?
首先,编译器会将var a = 2;解析成一个抽象语法树,这个抽象语法树会被转换成可执行代码。在这个过程当中,编译器解析var a的时候,会先去作用域中查看是否已经声明了一个变量名为a
的变量,如果没有,编译器会在作用域当中添加一个变量名为a的变量。接下来,编译器会为引擎生成运行所需要的代码。这些代码被用来处理a = 2,在引擎执行的之后,
会先询问作用域当中是否包含一个变量名为a的变量,如果有,则将它赋值为2,如果找到最顶层的作用域都没有找到这个变量,那么引擎会为它在全局作用域中创建一个变量,并且赋值为2。
LHS 查询和 RHS 查询
在编译器对var a = 2;进行解析的时候,会进行两个查询操作,分别为LHS和RHS。当对var a进行作用域查找的时候会进行LHS查询,而对a = 2进行查询的时候会进行RHS查询。
简单来说LHS查询是对变量在声明时候执行的查询操作,RHS查询在变量赋值的时候执行的查询操作。更准确一点,RHS 查询与简单的查找某一个变量的值别无二致,而 LHS 查询则是找到
变量容器本身,从而对其进行操作。
console.log(a);
在上面的代码当中,a的引用是一个 RHS 引用。
a = 2;
这里对a进行赋值是一个 LHS 引用。因为我们需要获取a容器的本身,对其进行赋值操作。
作用域嵌套
我们前面说过,作用域是根据名称查找变量的一套规则。在实际情况当中,通常需要同时对多个作用域进行查找。
当一个作用域嵌套在另一个函数/块作用域当中的时候,就发生了作用域嵌套。所以,当在当前作用域当中无法找到某个变量的时候,就会向外寻找,请看下面的例子
var c = 2;
function foo() {
function bar() {
var c = 5;
function baz() {
console.log(c);
}
baz();
}
bar();
}
foo(); // 5
上面的代码当中,baz嵌套早bar当中,而bar又放在foo当中。当我们需要在baz当中获取变量c的时候,会先在baz的作用域当中查询,如果没有找到,就向
上一层作用域查找,最终在bar的函数作用域当中找到了。