解析与编译
Javascript 从源程序到可以被计算机识别的目标程序主要包含两个阶段:
- 解析生成抽象语法树
- 编译执行
解析
以V8引擎为例,前置的解析被分为两种类型:Pre-Parser、Full-Parser。
Pre-Parser,主要负责对整个 Javascript 源代码进行必要的前期检查,判断是否存在语法错误。只在 Top-level 代码执行前进行。
这里是一种比较普遍的流程,在使用某个事物之前,靠谱的做法当然是先明确下能不能用。
Full-Parser,做的工作相应比较多些,包含:
- 通过分词/词法分析、解析/语法分析生成抽象语法树(Abstract Syntax Tree,AST)
-
进行作用域分析,为变量分配内存,生成可用的上下文作用域。具体包括:
- 将形参作为
GO/AO
的属性,赋值为实参值 - 将变量作为
GO/AO
的属性,赋值为undefind
- 将函数作为
GO/AO
的属性,赋值为其函数体 - 创建该函数的作用域链等
- 将形参作为
GO
(Global Object),全局环境下创建全局对象。AO
(Active Object),函数执行前创建激活对象。
Full-Parser,在Top-level代码和非Top-level代码执行前都会进行。函数在被调用执行前,经过Full-Parser生成抽象语法树提供给JIT编译器,生成目标语言执行。
Top-level 是指源代码初次加载时需要被首先运行到的“顶层”代码。
V8引擎不一次性完成 Javascript 源代码对应的 AST 信息,而是在知道要执行哪段代码前,将这段代码完成 AST 的生成。
想要了解 AST 信息,可查看 。
问题: 【1】变量提升的原因是因为为了提高执行效率,在代码执行前 Full-Parser阶段为变量分配资源。 【2】函数声明优先于变量声明是因为变量声明只检查变量是否存在,而函数声明需要更新变量值。
编译执行:
在了解JS的编译过程前,先明确两个概念:解释器、编译器。
解释器就像口译员,从源代码第一行开始进行解析编译执行。编译器则是直接将完整的源代码完全编译生成目标程序,从而快速执行。解释器与编译器各有各的优势,解释器能够快速启动与执行,浏览器能够快速执行JS代码对Web页面来说是非常重要的,这也是为什么浏览器使用解释器来解析JS源代码。但是,在使用解释器也存在着一些弊端,比如在处理循环的时候,解释器并没有很好的处理重复的“翻译”工作。所以在早期(2008年以前)JS执行的速度并不是很快。然而,编译器除了编译时间长一些,可以对代码有更好的优化,从而能够更快的执行代码。因而,在2008年,多种浏览器添加了即时编译器(JIT, just in time),使得JS的执行速度提高了10倍。那么JIT做了些什么事情呢?JIT包含两部分构成:- 基线编译器
- 优化编译器
首先源代码会经过基线编译器解析编译生成未优化的目标代码。同时JS引擎有称为监视器/分析器的部件,记录代码执行的次数和方式。
当某段代码执行次数变多时,如函数频繁调用、循环代码块等,基线编译器会对这段代码做一些优化。当这段代码执行次数越来越多,监视器会将这段代码交给优化编译器,从而生成更快的版本。为了生成更快的版本,优化编译器必须做一些假设,并且生成的代码也是默认这些假设都成立的。但是,如果在代码执行过程中,某个假设失败了,浏览器将执行返回到解释器或者基线编译的版本。这个过程称为去优化,所以循环中数据类型与结构的变化可能会对优化编译过程造成影响。具体流程如下: