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 是动态类型的,所以不会报任何错误。
这就意味着数字 2
会被转换成字符串,最终的结果将会变成 "12"
。引擎会还原之前优化过的 只接收两个数字 的类型反馈,并重新返回到字节码处运行。
本文首发于公众号:码力全开(codingonfire)
本文随意转载哈,注明原文链接即可,公号文章转载联系我开白名单就好~