这是我参与更文挑战的第23天,活动详情查看: 更文挑战
前言
我们都听说过 JS 引擎这个东西,但是相信对于大多数人来说,这是一个黑盒子,对其了解不深,反正我就把 JS 代码交给你了,你自己机灵点,执行好了再告诉我结果。那么对于 JS 引擎这个黑盒子,这篇文章也不会太过深入,只是可以让你对 JS 引擎这个东西有一个基本的了解。
JS 引擎是什么?
百度百科的话:JavaScript 引擎是一个专门处理 JavaScript 脚本的虚拟机,一般会附带在网页浏览器之中。
JS 引擎,即 JavaScript 虚拟机, 我们可以简单地把它理解成是一个 翻译程序,它将人类能够理解的编程语言 JavaScript 翻译成机器能够理解的机器语言。
现在市面上有很多种 JS 引擎,诸如 SpiderMonkey、Rhino、TraceMonkey、JaegerMonkey、JavaScriptCore、V8 等。我们最熟悉的听到最多的当然就是 V8 了,V8 是由谷歌开发的开源项目,是当下使用最广泛的 JavaScript 虚拟机,全球有超过 25 亿台安卓设备,而这些设备中都使用了 Chrome 浏览器,所以我们写的 JavaScript 应用,大都跑在 V8 上。
V8 的主要功能,就是结合 JavaScript 语言的特性和本质来编译执行它。如果我们了解 JavaScript 这门语言的一些基本特性和设计思想,那么我们去了解 V8 到底是怎么执行 JavaScript 代码的也将会变得简单一些。
语言类型
每种编程语言都具有内建的数据类型,它们的数据类型有很多不同之处,使用方式也很不一样。可以根据 数据类型是否需要在使用前确认 划分为静态语言和动态语言,根据 是否支持隐式类型转换 划分为弱类型语言和强类型语言。
- 在使用之前就需要确认其变量数据类型的称为静态语言
- 在运行过程中需要检查变量数据类型的语言称为动态语言
- 支持变量进行隐式类型转换的语言称为弱类型语言
- 不支持变量进行隐式类型转换的语言称为强类型语言
比如 C#、Java 就是强类型的静态语言;C、C++ 就是弱类型的静态语言;Python、Ruby 就是强类型的动态语言;JavaScript、PHP 就是弱类型的动态语言。
JavaScript 是一种弱类型的动态语言:
- 弱类型,意味着不需要告诉 JS 引擎变量是什么数据类型,它会在运行代码的时候自己检查计算出来,这一层计算也意味着要比强类型的语言多消耗一些性能。
- 动态,意味着可以用一个变量来保存不同类型的数据。
编译器和解释器
机器是不能够直接理解我们所写的代码的,所以 要在执行程序之前把代码转译成机器能读懂的机器语言。那么按照语言的执行流程,又可以把语言划分为 编译型语言 和 解释型语言。
编译型语言
编译型语言在程序执行之前,需要经过 编译器 的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样在每次运行程序的时候,都可以直接运行该二进制文件,就不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。
- 在编译型语言的编译过程中,编译器首先会依次对源代码进行 词法分析、语法分析,生成抽象语法树(AST),然后是 优化代码,最后再 生成处理器能够理解的机器码。
- 如果编译成功,将会生成一个可执行的文件。
- 但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。
解释型语言
解释型语言编写的程序,在每次运行时都需要通过 解释器 对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。
- 在解释型语言的解释过程中,同样解释器也会对源代码进行 词法分析、语法分析,生成抽象语法树(AST),不过它会 再基于抽象语法树生成字节码,最后 再根据字节码来执行程序、输出结果。
即时编译(JIT)
在 V8 出现之前,所有的 JavaScript 虚拟机所采用的都是 解释执行 的方式,这是 JavaScript 执行速度过慢的一个主要原因。而 V8 率先引入了 即时编译(JIT)的双轮驱动的设计,这是一种权衡策略,混合编译执行和解释执行这两种手段,给 JavaScript 的执行速度带来了极大的提升。
即时编译(JIT)是一种使用字节码配合解释器和编译器执行代码的技术,在 V8 里面,解释器在解释执行字节码的同时,会进行代码信息的收集,当它发现某一部分代码变热(一段代码被执行多次)了之后,编译器便会把热点字节码转换为机器码,并且把转换后的机器码保存起来,以备下次使用。
V8 执行一段 JavaScript 代码的过程
V8 执行一段代码的过程如下流程图所示:
- 将源代码转换为抽象语法树(AST),并且生成执行上下文
生成抽象语法树(AST)需要经过两个阶段(先分词,再解析):
- 第一阶段是 分词(
tokenize
),又称为 词法分析,它的作用是将一行行的源码拆解成一个个token
。所谓token
,指的是语法上不可能再分的、最小的单个字符或字符串。 - 第二阶段是 解析(
parse
),又称为 语法分析,它的作用是将上一步生成的token
数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。
这个过程是不是和 Babel 将 ES6 代码转换为 ES5 代码很像呢,Babel 的工作原理就是先将 ES6 源码转换为 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码。
- 解释器(Ignition)根据抽象语法树(AST)生成字节码,并解释执行字节码。
字节码是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
因为 V8 需要消耗大量的内存来存放转换后的机器码,所以 V8 团队是通过使用字节码来减少系统的内存使用,来解决内存占用的问题。
- 执行代码,输出结果
通常,如果有一段第一次执行的字节码,解释器(Ignition)会逐条解释执行。解释器(Ignition)除了负责生成字节码之外,它还有另外一个作用,就是 解释执行字节码。
在解释器(Ignition)执行字节码的过程中,如果发现有 热点代码(一段代码被重复执行多次),那么后台的编译器(TurboFan)就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
V8 执行时间越久,执行效率越高的原因就是更多的代码成为热点代码之后,转为了机器码来执行。
总结
- JS 引擎可以将高级编程语言 JavaScript 翻译成机器能够理解的机器语言,最具代表性的 JS 引擎是 V8.
- JavaScript 是一种弱类型的动态语言,运行的时候需要解释器对 JavaScript 进行动态解释和执行。
- V8 执行一段 JavaScript 代码的过程如下:
- 将源代码转换为抽象语法树(AST),并且生成执行上下文
- 解释器根据抽象语法树(AST)生成字节码,并解释执行字节码
- 执行代码,输出结果