前言
我们一直在写JS代码,但是不是发现自己却并不太明白我们写的JS代码究竟是如何执行的? 尤其是当被面试官问到这种问题时,如果一句话都搭不上,在面试官的心中会留下不好的印象的。
那么下面我们来弥补一下这方面的知识点吧~
V8
JS代码的执行离不开V8,当然最主要的原因是因为V8可以看懂我们写的js代码,并让它运行在浏览器上。
那么什么是V8呢?
V8是一款由Google开发的JavaScript引擎,主要用于在Chrome浏览器和Node.js中解释和执行JavaScript代码。它的作用包括:
提高JavaScript代码的执行速度:V8使用了一系列先进的编译技术和优化算法,可以将JavaScript代码转换为高效的本地机器码,从而提高代码的执行速度。支持新的JavaScript特性:V8支持许多新的JavaScript特性,如箭头函数、模板字面量、解构赋值等,使得开发者可以更加方便地编写现代化的JavaScript代码。提供开发者工具:V8提供了一系列开发者工具,如Chrome DevTools和Node.js的调试器,可以帮助开发者更加方便地调试JavaScript代码。
其核心功能是执行易于人类理解的 JavaScript 代码。
那么V8 又是怎么执行 JavaScript 代码的呢?
要深入理解 V8 的工作原理,需要搞清楚一些概念和原理,比如编译器(Compiler)、解释器(Interpreter)、抽象语法树(AST)、字节码(Bytecode)、即时编译器(JIT) 等概念。
编译器和解释器
之所以存在编译器和解释器,是因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将我们所写的代码“翻译”成机器能读懂的机器语言。按语言的执行流程,可以把语言划分为编译型语言和解释型语言。
编译型语言:在代码运行前编译器直接将对应的代码转换成机器码,运行时不需要再重新翻译,直接可以使用编译后的结果;解释型语言:需要将代码转换成机器码,和编译型语言的区别在于运行时需要转换。解释型语言的执行速度要慢于编译型语言,因为解释型语言每次执行都需要把源码转换一次才能执行。
Java 和 C++ 等语言都是编译型语言,而 JavaScript 是解释性语言,它整体的执行速度会略慢于编译型的语言。
编译型语言和解释器语言代码执行的具体流程如下:
两者的执行流程如下:
在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。
在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。
这里可以看到它们二者的主要区别是,编译型语言是可以直接生成CPU处理器能够理解的机器码,解释型语言是先生成字节码,再生成机器码。至于为什么,下面还会继续讨论~
V8 执行JavaScript代码过程
可以先来“一览全局”,参考下图:
V8 在执行过程用到了解释器 Ignition和编译器 TurboFan。 其执行过程如下:
- Parse 阶段:V8 引擎将 JS 代码转换成 AST(抽象语法树);
- Ignition 阶段:解释器将 AST 转换为字节码,解析执行字节码也会为下一个阶段优化编译提供需要的信息;
- TurboFan 阶段:编译器利用上个阶段收集的信息,将字节码优化为可以执行的机器码;
生成抽象语法树(AST)
抽象语法树(Abstract Syntax Tree,AST) 是源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
AST 运用广泛,比如:
- 编辑器的错误提示、代码格式化、代码高亮、代码自动补全;
elint、pretiier对代码错误或风格的检查;webpack通过babel转译javascript语法;
AST 如何生成
js执行的第一步是读取 js 文件中的字符流,然后通过词法分析生成分词(token),之后再通过语法分析( Parser)生成 AST,最后生成机器码执行。
可以看到生成AST要经过两个步骤
-
第一阶段是分词(tokenize),又称为词法分析: 其作用是将一行行的源码拆解成一个个
token。所谓 token,指的是语法上不可能再分的、最小的单个字符或字符串。比如代码 var a = 1;
通常会被分解成 var 、a、=、1、; 这五个词法单元。
代码中的空格在 JavaScript 中是直接忽略的,简单来说就是将 JavaScript 代码解析成一个个令牌(Token)。
-
第二阶段是解析(parse),又称为语法分析,其作用是将上一步生成的
token数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。代码 var a = 1;会转化为下面这样的令牌:
Keyword(var) Identifier(name) Punctuator(=) Number(1)
这就是 AST 的生成过程,先分词,再解析
生成字节码
有了 抽象语法树 AST 后,就轮到解释器就登场了,它会根据 AST 生成字节码,并解释执行字节码。
这里回到刚开始编译型语言和解释型语言的区别,它们最大的区别就是解释型语言不是直接生成机器码,而是先生成字节码,再转换成机器码。
引入字节码的目的
v8引入字节码的目的是优化javascript代码执行的性能和启动速度。
在传统的解释执行模式下,每次运行javascript代码都需要将其转换为机器代码,这个过程比较浪费时间。
而使用字节码,则可以将中间代码保存下来,在下一次执行相同的代码时直接使用已经编译好的字节码,跳过了解释和编译的过程,从而提高执行效率。
如何为保存中间代码
通常,如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。在 Ignition 执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
字节码配合解释器和编译器是最近一段时间很火的技术,比如 Java 和 Python 的虚拟机也都是基于这种技术实现的,我们把这种技术称为即时编译(JIT)。
具体到 V8,就是指解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。
即时编译(JIT)技术流程图
所以一段JS代码执行过程,大致如下:
优化和反优化
在解释和执行AST过程中,解释器会将一些重复可优化的操作收集起来生成分析数据,然后将生成的字节码和分析数据传给编译器,编译器会依据分析数据来生成高度优化的机器码。
优化后的机器码的作用和缓存很类似,当解释器再次遇到相同的内容时,就可以直接执行优化后的机器码。当然优化后的代码有时可能会无法运行(比如函数参数类型改变),那么会再次反优化为字节码交给解释器。
小结
这部分偏底层一点的知识点也是需要我们在面试的时候能够游刃有余的说出来的,而且只要花一点时间就可以掌握的内容,性价比高~
希望能帮助到屏幕前的友友~
谢谢!!!
🌹🌹🌹
本文正在参加「金石计划」