JS的编译原理

186 阅读4分钟

完成一个编译器大抵上主要有这几部分

  • 词法分析

一般用有限状态自动机或者手工编写来实现,这一步输出的是token序列

  • 语法分析

主要分为自顶向下和自底向上的语法分析,一般有递归下降,LL(1),LR(1),LALR(1)几种方法实现。这一步输出的是语法树

  • 语义分析

语义分析主要任务是生成符号表,并且发现不符合语义的语句,这一步输出的还是AST

  • 代码生成

这里一般会生成一个与平台无关的较为贴近底层的中间语言(IR),这一步输入AST,输出的是IR

  • 代码优化

这一步的工作和名字一样,就是进行代码的优化,提升性能等等

  • 目标代码生成

这一步的任务就是生成平台相关的汇编语言了

以上差不多就是整个通用意义上来说的编译器了,但是也可以把包括调用链接器汇编器来生成可执行文件

对于常见编译型语言(例如:Java)来说,编译步骤分为:词法分析->语法分析->语义检查->代码优化和字节生成。

对于解释型语言(例如JavaScript)来说,通过词法分析和语法分析得到语法树后,就可以开始解释执行了。

JavaScript运行

一、编译阶段

  1. 词法、语法分析
    js引擎检查你的代码有没有什么低级的语法错误,生成AST

  2. 预编译
    预编译简单理解就是在内存中开辟一些空间,存放一些变量与函数(变量提升、函数提升、静态作用域确定)
    遇到函数时 预编译什么时候发生?
    遇到函数的时候其实只有在执行函数的时候才会预编译那一部分

二、解释执行阶段

  1. 解释执行
    执行代码

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

 JavaScript引擎在执行每个函数实例时,都会创建一个执行环境(execution context)。执行环境中包含一个调用对象(call object), 调用对象是一个scriptObject结构(“运行期上下文”),用来保存内部变量表varDecls、内嵌函数表funDecls、父级引用列表upvalue等语法分析 结构(注意:varDecls和funDecls等信息是在语法分析阶段就已经得到,并保存在语法树中。函数实例执行时,会将这些信息从语法树复制到 scriptObject上)。scriptObject是与函数相关的一套静态系统,与函数实例的生命周期保持一致,函数执行完毕,该对象销毁。

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

当代码在一个环境中执行时,会创建一个变量对象的作用域链,作用域的前端是当前执行环境的活动对象, 作用域的尾端,是全局执行环境的的变量对象,也就是window对象。作用域链的作用是保证执行环境对有权访问的变量和函数的有序访问。

例如:

function a(arg){
    console.log(arg);
}

全局环境的变量对象始终存在,而像上述的a函数这样的局部环境的变量对象,则只有在函数执行的过程中存在。

创建a函数时,就会创建一个预先包含全局变量对象的作用域链,并保存在内部的[[scope]]属性中,当调用a函数时,会为函数创建一个执行环境,然后复制函数的[[scope]]属性中的对象构建起执行环境的作用域链,然后又有一个活动对象(在此作为变量对象使用,也就是a的变量对象)被创建并推入作用域链的前端。因此,对于这个例子a函数的执行环境来说,其作用域包含两个变量对象,本地变量对象和全局变量对象。并且可见,作用域链本质上是一个指向变量对象的指针链表。