前言
JS 实在是太酷了(认真脸),那你有没有想过机器是怎么解析 JS 代码的?作为一个 JS 开发者,一般我们不需要直接跟编译器打交道,但是如果可以了解其中的基本原理,相信会对以后的工作和学习都有帮助的!
本篇介绍的知识主要基于 Node.js 和基于 Chromium 的浏览器所用的 V8 引擎
生成抽象语法树
HTML 解析器在遇到script标签时,便会加载其中的代码。代码可能是从网络请求、缓存或者Service Worker中加载的。由于代码是以字节流的形式响应回来的,所以当代码下载完成后就会交给字节流解码器。
词法分析
生成抽象语法树的第一个阶段是分词 (tokenize),又叫词法分析
字节流解码器会先从代码字节流中创建令牌 (token)
注:令牌可以理解为语法上不可能再分的,最小的单个字符或字符串)。
如:0066解码为f,0075解码为u,0063解码为c,0074解码为t,0069解码为i,006f解码为o,006e解码为n同时后面跟一个空格。然后你就得到了关键字function!
每当一个令牌创建后,就会被传递给解析器(parser)。具体见下图:
语法分析
第二个阶段是解析(parse),也叫语法分析
引擎其实使用了两个解析器。一个是预解析器,一个是解析器。
预解析器会先检查源码是否符合语法规则,如果不符合就直接抛出错误。这个提前检查机制可以提高解析器的效率。
如果没有错误,解析器便会根据传过来的令牌创建出抽象语法树 (Abstract Syntax Tree)并生成执行上下文(关于执行上下文的知识我们有机会再讲)
生成字节码
AST 被生成之后,接下来就要交给解释器(interpreter)了。解释器会遍历整个 AST,并生成字节码。当字节码生成后,AST 便会被删除以节省内存空间。最终我们得到了更贴近机器码的字节码。
这里的字节码是介于AST和机器码之间的一种代码,它还是需要通过解释器将其转换为机器码后才能执行
执行代码
生成了字节码之后,就可以进入执行阶段了。执行阶段过程中引擎会做一些优化操作,一个是即时编译,一个是内联缓存。
即时编译
尽管字节码很快,但是它还可以更快!解释器在逐条解释执行字节码时,会分析是否有某段代码被多次执行,这样的代码被称为热点代码。
热点代码和生成的类型反馈 (type feedback)会被发送到一个称为优化编译器的东西中,然后由它转换为可以直接被电脑执行的机器码,这样在下次执行这段代码的时候就不需要再编译了,从而大大提升了代码的执行效率。
这种技术也被称为即时编译(JIT:Just In Time),而上面所说的优化编译器也叫JIT 编译器。
内联缓存
JavaScript 是一种动态类型的语言,这意味着数据类型可以不断变化。如果 JS 引擎每次都要检查数据的类型,那速度将会非常慢。
所以引擎就使用了一种叫做内联缓存 (inline caching)的技术。它将代码缓存在内存中,以便将来可以针对相同的行为直接返回缓存的值。比如你有一个函数调用了 100 次,每次都返回同一个值,那么引擎就会假定在 101 次时也返回该值。
假设我们有一个求和函数sum,每次都接收两个数字:
上面的函数返回值为3!下次我们调用它时,引擎会假定我们还是传入两个数字类型的参数。
如果假设正确,就省去了动态查询阶段。引擎就可以直接使用存储在内存中的结果。否则,引擎会还原到原始字节码处解释执行,而不是使用优化过的机器码。
比如,下次我们要调用求和函数时,传入了一个字符串和一个数字,由于 JS 是动态类型的,所以不会报任何错误。
网友评论