编译原理
作用域:是一套设计良好的规则,用来存储变量,并且之后可以方便地找到这些变量。
传统编译语言的流程中,程序中一段源代码在执行之前会经历三个步骤,统称为“编译”。
- 分词/词法分析(Tokenizing/Lexing):将程序中字符串分解成词法单元;
- 解析/语法分析(Parsing):将词法单元流(数组)转换成一个元素逐级嵌套所组成的代表了程序语法结构的树,这个树成为抽象语法树(AST);
- 代码生成:将AST转换成可执行代码的过程称为代码生成。
JavaScript的编译过程不是发生在构建之前,不会有大量时间来优化,任何JavaScript代码片段在执行前(几微秒)都要编译。
理解作用域
本书学习作用域的方式是将这个过程模拟成几个人物之间的对话。
演员表
首先介绍将参与到对程序var a=2;
进行处理的过程中的演员们:
- 引擎
从头到尾负责整个JavaScript程序的编译及执行过程 - 编译器
负责语法分析及代码生成 - 作用域
负责收集并维护所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
对话
面对var a=2;
时,引擎会认为这里有两个完全不同的声明:
一是由编译器在编译时处理;
二是则由引擎在运行时处理。
编译器、引擎的工作过程:
- 遇到
var a
,编译器首先查询作用域是否存在该名称变量,如果有,编译器则忽略该声明;否则声明一个新变量a
; - 接下来编译器会对
a=2
这个赋值操作进行代码生成工作。引擎运行时会首先询问作用域是否存在该变量,如果有,引擎会使用这个变量;如果没有,引擎继续查找,查不到就抛出一个异常。
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量,然后在运行时引擎会在作用域中查找该变量,如果能够找到就对它赋值。
编译器有话说
编译器在编译过程中代码生成后,引擎执行时,会通过查找变量a
来判断它是否已经声明。查找的过程由作用域进行协助,但有两种查找类型:LHS
、RHS
:LHS
查询是试图找到变量的容器本身,RHS
查询就是简单的查找某个变量。
1 | function foo(a){ |
作用域的嵌套
作用域是根据名称查找变量的一套规则。实际情况中,通常需要同时顾及几个作用域。
遍历嵌套作用域链的规则:引擎从当前执行的作用域开始查找变量,如果找不到就向上一级继续查找,当找到最外层的全局作用域时,无论找没找到,都会停止。
异常
为什么区分LHS和RHS是一件重要的事?
在变量还没有声明(任何作用域中都无法找到该变量)情况下,RHS是会抛出ReferenceError异常;LHS则是在全局作用域中创建一个具有该名称的变量,并返给引擎(这就是隐式的全局变量的由来)。
严格模式下,会禁止自动或隐式的创建全局变量,所有LHS查询失败时也会抛出ReferenceError异常。
当LHS和RHS查询成功,但对变量的值进行不合理的操作,会抛出TypeError。
小结
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。
JavaScript引擎首先会在代码执行前对其进行编译,在这个过程中,像var a=2
这样的声明会被分解成两个独立的步骤:
- 首先, var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。(变量声明提升的由来)。
- 接下来, a = 2 会查询(LHS查询)变量a 并对其进行赋值。
不成功的RHS引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下) , 该变量使用LHS引用的目标作为标识符, 或者抛出ReferenceError 异常(严格模式下) 。