编译原理
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。
- 分词/词法分析(Tokenizing/Lexing)
这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被
称为词法单元(token)。例如,考虑程序var a = 2;。这段程序通常会被分解成为下面这些词法
单元:var、a、=、2 、;。空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。
解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的
树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
var a = 2;的抽象语法树中可能会有一个叫作VariableDeclaration的顶级节点,接下来是一个
叫作Identifier(它的值是a)的子节点,以及一个叫作AssignmentExpression的子节
点。AssignmentExpression节点有一个叫作NumericLiteral(它的值是2)的子节点。代码生成
将AST转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息息相关。
抛开具体细节,简单来说就是有某种方法可以将var a = 2;的AST转化为一组机器指令,用来
创建一个叫作a的变量(包括分配内存等),并将一个值储存在a中。
理解作用域
学习作用域的方式是将这个过程模拟成几个人物之间的对话
演员表
对程序var a = 2;进行处理的过程中的演员;
- 引擎
从头到尾负责整个JavaScript程序的编译及执行过程。 - 编译器
引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。 - 作用域
引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施
一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
对话
将var a = 2;分解事实上编译器会进行如下处理。
遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。
如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一
个新的变量,并命名为a。接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作。引擎
运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果否,引擎就会
使用这个变量;如果不是,引擎会继续查找该变量(查看1.3节)。如果引擎最终找到了a变量,就会将2赋值给它。否则引擎就会举手示意并抛出一个异常!
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没
有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
编译器有话说
在我们的例子中,引擎会为变量a进行LHS查询。另外一个查找的类型叫作RHS,“L”和“R”的含义,它们分别代表左侧和右侧。是一个赋值操作的左侧和右侧。换句话说,当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。
讲得更准确一点,RHS查询与简单地查找某个变量的值别无二致,而LHS查询则是试图找到变量
的容器本身,从而可以对其赋值。
console.log( a );对a的引用是一个RHS引用,a = 2;这里对a的引用则是LHS引用
引擎和作用域的对话
function foo(a) {
console.log( a ); // 2
}
foo( 2 );
让我们把上面这段代码的处理过程想象成一段对话,这段对话可能是下面这样的。
引擎:我说作用域,我需要为foo进行RHS引用。你见过它吗?
作用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。
引擎:哥们太够意思了!好吧,我来执行一下foo。
引擎:作用域,还有个事儿。我需要为a进行LHS引用,这个你见过吗?
作用域:这个也见过,编译器最近把它声名为foo的一个形式参数了,拿去吧。
引擎:大恩不言谢,你总是这么棒。现在我要把2赋值给a。
引擎:哥们,不好意思又来打扰你。我要为console进行RHS引用,你见过它吗?
作用域:咱俩谁跟谁啊,再说我就是干这个的。这个我也有,console是个内置对象。给你。
引擎:么么哒。我得看看这里面是不是有log(..)。太好了,找到了,是一个函数。
引擎:哥们,能帮我再找一下对a的RHS引用吗?虽然我记得它,但想再确认一次。
作用域:放心吧,这个变量没有变动过,拿走,不谢。
引擎:真棒。我来把a的值,也就是2,传递进log(..)。
作用域嵌套
作用域是根据名称查找变量的一套规则,当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作
用域(也就是全局作用域)为止。
遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一
级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
小结
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行
赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
赋值操作符会导致LHS查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值
操作。
JavaScript引擎首先会在代码执行前对其进行编译,在这个过程中,像var a = 2这样的声明会被分
解成两个独立的步骤:
- 首先,var a在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
- 接下来,a = 2会查询(LHS查询)变量a并对其进行赋值。
LHS和RHS查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识
符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层楼),最后抵达全局
作用域(顶层),无论找到或没找到都将停止。
不成功的RHS引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个
全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError异常
(严格模式下)。
One More Thing
本文为原创文章,大部分来自于《你不知道的javascript》一书中,转载请注明出处https://xdlmr.github.io