JavaScript解析基础

课后整理 2020-12-14

JavaScript解析过程可以分为编译和执行两个阶段。编译也就是我们常说的JavaScript预处理(即预编译)。在预编译期,JavaScript解释器将完成对JavaScript代码的预处理,也就是说把JavaScript脚本代码转换成字节码。在执行期,JavaScript解释器借助执行期环境把字节码生成机械码,并按顺序执行,完成程序设计的任务。

预编译

JavaScript是一种解释型语言,而不是编译型语言。所谓解释型语言,就是代码在执行时才被解释器一行行动态编译和执行,而不是在执行之前就完成编译。简单说,解释型语言就是边解析边执行,而编译型语言是先编译后执行,两者的操作过程不同。

当程序被编译时,需要一个叫做编译器的程序来完成所有工作。一般编译器可以包括下面组件(如图1所示):

图1  编译器构成和工作流程示意图

根据上图,我们可以看到,程序的一般编译步骤分为:词法分析、语法分析、语义检查、代码优化和生成字节码。但是,对于JavaScript这类解释型语言来说,通过词法分析和语法分析,并建立语法树之后,就开始解释执行了,而不是完全生成字节码之后,再调用虚拟机来执行这些编译好的字节码。

在词法分析过程中,JavaScript解释器先把脚本代码的字符流转换为记号流,例如:

把字符流:

a = (b - c);

转换为记号流:

NAME "a" 
EQUALS 
OPEN_PARENTHESIS 
NAME "b" 
MINUS 
NAME "c" 
CLOSE_PARENTHESIS 
SEMICOLON

词法分析器是编译器中与源程序直接接触的部分,因此词法分析器可以实现:

词法结构是JavaScript语言基础(详细讲解请参阅第3章内容),至于词法分析的实现就比较复杂,这里就不再深入研究,读者只需要简单了解它的工作机制即可。

词法分析和语法分析不是完全独立的,而是交错进行的,也就是说,词法分析器不会读取所有的词法记号,然后再使用语法分析器来处理,通常情况下,每取一个词法记号,就送入语法分析器进行分析(如图2所示)。

图2  词法分析和语法分析示意图

词法分析是对JavaScript脚本代码进行逐一分析的过程,它相当于语言翻译,例如,把英文逐词逐句的译成中文,英文就是源代码,而中文就是代码的记号了。

语法分析的过程就是把词法分析所产生的记号生成语法树。通俗说就是把从程序中收集的的信息存储到数据结构中。请注意,编译中的数据结构包括两种:符号表和语法树。

例如,下面是一个简单的条件结构和输出信息代码段,被语法分析器转换为语法树之后,如图3所示。

if(typeof a ==  "undefined" ){
   a = 0;
}
else{
   a = a;
}
alert(a);

图3  语法树结构示意图

当JavaScript解释器在构造语法树的时候,如果发现无法构造,就会报语法错误,并结束整个代码块的解析。对于传统强类型语言来说,通过语法分析,构造出语法树后,翻译出来的句子可能还会有模糊不清的地方,还需要进一步的语义检查。语义检查的主要部分是类型检查,例如,函数的实参和形参类型是否匹配。但是,对于弱类型语言来说,这一步就没有了。

执行期

经过编译阶段的准备,JavaScript代码在内存中已经被构建为语法树,然后JavaScript引擎就会根据这个语法树结构边解释边执行了。

在解释过程中,JavaScript引擎是严格按着作用域机制来执行的。JavaScript语法采用的是词法作用域,也就是说JavaScript的变量和函数作用域是在定义时决定而不是执行时决定,由于词法作用域取决于源代码结构,所以JavaScript解释器只需要通过静态分析就能确定每个变量、函数的作用域,这种作用域也称为静态作用域。

当JavaScript解释器执行每个函数时,先创建一个执行环境,在这个虚拟环境中创建一个调用对象,在这个对象内存储着当前域中所有局部变量、参数、嵌套函数、外部引用和父级引用列表upvalue等语法分析结构。

实际上,通过声明语句定义的变量和函数在预编译期的语法分析中就已经存储到符号表中了,然后把它们与调用对象中的同名属性进行映射即可。调用对象的生命周期与函数的生命周期是一致的,当函数调用完毕,且没有外部引用的情况下,会自动被JavaScript引擎当做垃圾进行回收。

另外,JavaScript解释器通过作用域链把多个嵌套的作用域串连在一起,并借助这个链条帮助JavaScript解释器检索变量的值。这个作用域链相当于一个索引表,并通过编号来存储它们的嵌套关系。当JavaScript解释器检索变量的值,会按着这个索引编号进行快速查找,直到找到全局对象为止,如果没有找到值,则传递一个特殊的undefined值。

如果函数引用了外部变量的值,则JavaScript解释器会为该函数创建一个闭包体,闭包体是一个完全封闭和独立的作用域,它不会在函数调用完毕后就被JavaScript引擎当做垃圾进行回收。闭包体可以长期存在,因此开发人员常把闭包体当做内存中的蓄水池,专门用来长期保存变量的值。

只有当闭包体的外部引用被全部设置为null值时,该闭包才会被回收。当然,也容易引发垃圾泛滥,甚至出现内存外溢现象。