前言:
所有的编程语言都具备一个基本功能,储存、访问、修改变量的值,这种能力将状态带给了程序。
那问题来了:变量储存在哪里?如何访问到它们?
一,编译原理
一段源代码在执行前会经历的三个步骤,统称为编译。
1,传统编译语言(这里以执行“var a = 2;”为例)
1️⃣分词/词法分析(Tokenizing/Lexing):
词法单元生成器会将字符串分解成一个一个代码块,称为词法单元。如:var、a、=、2、;。一共5个,空格是否被当作词法单元取决于空格在这门语言中是否有意义。
这里译为两个单词,说明这其实是两个步骤,但是在编译过程中两者放在了同一个步骤里,说明两者差异性并不是很大,并且两者的功能或者目的实际上是差不多的,那么分词和词法分析的异同是什么:这里我的理解是词法分析是为了更好的分词,两者其实是一个过程,唯一的区别是,分词只是简单地把字符串拆开来,拆成一个一个代码块,这个过程是无状态的,它不会去识别这些词法单元究竟是独立的还是和其它词法单元有关联(比如某个词法单元是另一个的一部分,它并非独立)。而词法分析就是调用有状态的解析规则,在分词的基础上再进行一次分析,两者实际上是做的同一块东西。
贴一张图:
2️⃣解析/语法分析(Parsing):
上一步分析完的一个一个的词法单元,会组成一个词法单元流(数组),这一步会将这个流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树,这个树称为“抽象语法树”(AST)。以var a = 2; 为例:
这里再贴一张语法结构图,更清晰:
3️⃣代码生成:
将AST转换成可执行代码的过程被称为代码生成,简单来说,就是将var a = 2;的AST转化成一组机器指令,用来创建一个叫作a的变量(包括内存分配等),并将一个值储存在a中。
普通编译器的编译过程一般就是以上三步,(上面的图片中的解析器链接:http://esprima.org/demo/parse.html#)而JavaScript引擎要复杂得多,在语法分析和代码生成阶段有特定步骤来对运行性能进行优化,对于JavaScript来说,编译发生在代码执行前的几微秒(甚至更短)。
二,引擎、编译器和作用域
编译器:上面刚刚提到的三个步骤就是编译器做的事情。
引擎:负责整个JavaScript程序的编译及执行过程。(主要是执行)
作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
例如,执行var a = 2;的时候:
1,编译器先进行编译(上面的三步),前两步一致(分词/词法分析、解析AST),但是当执行到第三步的时候(代码生成),有不同了:
1️⃣遇到var a,编译器会先询问作用域是否已经有一个变量a在该作用域的集合中,如果是,则忽略该声明(因为作用域里已经有了一个a,不用再声明一个a,其实也等同于不需要重新分配内存了),如果不是,则在当前作用域下声明一个新的变量,命名为a。
2️⃣接下来就是为引擎生成运行时所需的代码,也就是机器指令,它会告诉引擎“要给a这个变量赋值2”。
2,上面就是编译器做的事情,而引擎在运行时会先询问作用域,在当前作用域的集合中是否存在一个叫作a的变量,如果是,引擎就会使用这个变量,如果不是,引擎会继续查找该变量(沿着作用域链)。引擎最终如果找到了a变量,就会将2赋值给它,如果没有找到,则会抛出一个异常。
总结就是,编译器只负责编译部分,它不会去执行代码,而是把js语言编译成引擎能读懂的机器指令,而引擎负责执行这一系列指令。作用域的作用其实就比较大了,无论是在编译器编译的时候还是在引擎执行的时候,都起到了关键作用。
待续。。。
end