JavaScript代码是如何被执行的

基本概念

  • 编译器,解释器
  • 抽象语法树
  • 字节码和机器码

编译器和解释器

计算机不能直接理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言编写的程序。根据语言的执行流程,可以把语言分成编译型语言和解释型语言。

编译型语言:程序在执行之前需要一个专门的编译过程,把程序编译成 为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了。程序执行效率高,依赖编译器,跨平台性差些。如C、C++、go等.

解释型语言: 程序不需要编译,程序在运行时才翻译成机器语言(所以执行前需要环境中安装了解释器),每执行一次都要翻译一次。因此效率比较低。效率比较低,依赖解释器,跨平台性好

编译型与解释型,两者各有利弊, 不能一概而论。前者由于程序执行速度快,同等条件下对系统要求较低,因此像开发操作系统、大型应用程序、数据库系统等时都采用它,像C/C++、Pascal/Object Pascal(Delphi)等都是编译语言,而一些网页脚本、服务器脚本及辅助开发接口这样的对速度要求不高、对不同系统平台间的兼容性有一定要求的程序则通常使用解释性语言,如JavaScript、VBScript、Perl、Python、Ruby、MATLAB 等等。

编译器和解释器

我们都知道 JavaScript 存在变量提升,在函数作用域内的任何变量的声明都会被提升到顶部并且值为 undefined。
所以JS引擎好像对同一个脚本执行了两次,第一次完成所有声明,然后第二次才执行代码?还是先编译整个代码然后运行它?这两种都不对。

其实变量声明不过只执行上下文的小把戏。在执行任何语句之前,解释器就要从创建执行上下文后已经存在的作用域中找到变量的值。

抽象语法树

抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有两个分支的节点来表示。

字节码和机器码

字节码(Byte-code):是一种包含执行程序、由一序列 op 代码/数据对组成的二进制文件。字节码是一种中间码,它比机器码更抽象。

机器码 (Machine-code):计算机直接使用的程序语言,其语句就是机器指令码,机器指令码是用于指挥计算机应做的操作和操作数地址的一组二进制数。

JavaScript代码执行过程

  1. 生成AST(抽象语法树)
  2. 生成字节码
  3. 执行代码

生成AST

生成AST的步骤可以拆分成以下两个小步骤:

  1. 词法分析:将JavaScript代码解析成一个个词法单元(token)
  2. 语法分析:将词法单元根据一定规则组装成抽象语法树

通过 javascript-ast 网站,可以大概了解 代码生成的 Tokens 以及 AST大致的样子。

  1. 词法分析:将JavaScript代码解析成一个个词法单元(token)

例如let a = 2;,通常会被分解为下面这些词法单元 leta=2; 空格是否会被当做词法单元取决于空格在这门语言中是否会具有意义。

分解Token示意图
  1. 语法分析:将词法单元根据一定规则组装成 AST
let a = 2;
console.log(a);

我们可以看到生成的AST结构如下:

AST

高级语言是开发者可以理解的语言,编译器和解释器理解不了。所以无论你使用的是解释型语言还是编译型语言,在编译过程中,它们都会生成一个 AST。当生成 AST之后,编译器/解析器后续的工作都要依靠 AST而不是源码。

AST是一个非常重要数据结构,比如Babel的工作原理就是: ES6 的代码解析成 AST -> 将 ES6 的 AST 转换成 ES5 的AST -> 将 ES5的 AST 转成 ES5的代码。Babel的相关文章推荐 深入浅出 Babel 上篇:架构和原理 + 实战;我们使用的 Eslint(检查JavaScript编写规范的插件) 的检测流程也是先将源码转换成 AST, 然后利用 AST 来检查代码规范的问题

生成字节码

JavaScript引擎通过解释器来将 AST 转换成字节码,字节码是无法直接执行的,需要将其转为机器码才能直接执行。V8早期的时候,是直接将AST转成机器码的,后来因为 V8 需要消耗大量的内存来存放转换后的机器码,导致严重的内存占用问题。为了解决这个问题,引入 了字节码。字节码是比机器码轻量得多的代码。

字节码是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换成机器码后才能执行。

执行代码

生成字节码之后,就到了解释和执行字节码阶段了,

监听热点代码并优化为二进制机器码

解释器会逐条执行字节码,(解释器除了负责生成字节码,还会负责解释执行机器码) 如果发现一段代码重复执行多次,就会它记为热点代码(HotSpot),V8会将这段热点代码提交给优化编辑器,优化编辑器会在后台将字节码编译为二进制代码,然后在对编译后的二进制代码执行优化操作,并保存下来。保存下来的机器码的作用和缓存很类似,当解释器再次遇到相同的内容时,就可以直接执行保存下来的机器码。

这样代码执行得越久,执行效率就会越快,因为会有越来越多的字节码被标记为 热点代码,遇到他们就可以直接执行,而不用转成机器码。

反优化生成的二进制机器码

JavaScript是一种非常灵活的动态语言,对象的结构和属性在运行时任意被改变,而经过优化后的代码只能针对某种固定结构。一旦在执行过程中,对象的结构被动态修改了,那么优化后的代码会变成无效的代码,这时候优化编辑器就需要执行反优化操作,经过反优化的代码下次执行时就会回退到解释器解释执行。

字节码的执行是需要配合编译器和解释器的(这种技术称为即时编译 JIT)所以之前说 JS是一种解释型语言并不准确。

总结

整个过程如下面流程图所示:

JavaScript代码执行过程

参考