持续创作,加速成长!这是我参与「掘金日新计划 · 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
的函数作用域当中找到了。