首页 » 编写高质量代码:改善JavaScript程序的188个建议 » 编写高质量代码:改善JavaScript程序的188个建议全文在线阅读

《编写高质量代码:改善JavaScript程序的188个建议》建议166:掌握JavaScript预编译过程

关灯直达底部

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

JavaScript是一种解释型语言,而不是编译型语言。所谓解释型语言,就是代码在执行时才被解释器逐行动态编译和执行,而不是在执行之前就完成编译。而编译型语言是先编译后执行,两者的操作过程不同。当程序被编译时,需要一个被称为编译器的程序来完成所有工作。一般编译器可以包括下面一些组件(如图9.2所示)。

❑符号表:其中存储所有的符号及其信息,如类型、范围等。

❑词法分析器:其功能是将字符流(即脚本字符串)转换为记号(如关键词、操作符等)。

❑语法分析器:其功能是读取记号流,并建立语法树。

❑语义检查器:用来检查语法树的语义错误。

❑中间代码生成器:用来把语法树转换为中间代码。

❑代码优化器:用来优化中间代码。

❑代码生成器:用来将中间代码生成二进制字节码。

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

编译程序的一般步骤分为:词法分析、语法分析、语义检查、代码优化和生成字节码。但是,对于JavaScript这类解释型语言来说,通过词法分析和语法分析,并建立语法树之后,就开始解释执行了,而不是在完全生成字节码之后,再调用虚拟机来执行这些编译好的字节码。在词法分析过程中,JavaScript解释器先把脚本代码的字符流转换为记号流。例如,把字符流:


a=(b-c);


转换为记号流:


NAME/"a/"

EQUALS

OPEN_PARENTHESIS

NAME/"b/"

MINUS

NAME/"c/"

CLOSE_PARENTHESIS

SEMICOLON


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

❑去掉注释,自动生成文档。

❑提供错误位置(可以通过记录行号来提供),当字符流变成词法记号流以后,就没有了行的概念。

❑完成预处理,如C语言中的宏定义等。

词法结构是JavaScript语言基础,至于词法分析的实现就比较复杂,这里就不再深入研究,读者只需要简单了解它的工作机制即可。

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

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

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

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

❑符号表:就是在程序中用来存储所有符号的一个表,包括所有的字符串变量、直接量字符串,以及函数和类。

❑语法树:就是程序结构的一个树形表示,用来生成中间代码。

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


if(typeof a==/"undefined/"){

a=0;

}

else{

a=a;

}

alert(a);


图 9.4 语法树结构示意图

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

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

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

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

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

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

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

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