理解v8引擎原理
1. JavaScript由谁来运行
1.1. 浏览器执行js过程
渲染引擎+js引擎
渲染引擎:用来解析html+css,俗称浏览器内核,比如chrome浏览器的blink,老版本的Webkit
js引擎:用来读取网页中的JavaScript代码,对其处理后运行
浏览器本身并不会执行js代码,而是通过
内置JavaScript引擎来执行js代码js引擎执行代码时逐行解释每一句源码,将其转换为机器语言,然后由计算机去执行,所以JavaScript语言归为脚本语言,会
逐行解释执行
1.2. JavaScript引擎
为什么需要JavaScript引擎
- 高级的编程语言都是需要转成最终的机器指令来执行的
- 编写的JavaScript无论交给浏览器或者Node执行,最后都是需要被CPU执行的
- 但是CPU只认识自己的指令集,实际上是机器语言,才能被CPU所执行
- 所以需要JavaScript引擎将JavaScript代码翻译成CPU指令来执行
常见的JavaScript引擎
- SpiderMonkey:第一款JavaScript引擎,由Brendan Eich开发(也就是JavaScript作者)
- Chakra:微软开发,用于IT浏览器
- JavaScriptCore:WebKit中的JavaScript引擎,Apple公司开发
- V8:Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出
- …
1.3. 浏览器内核和JS引擎的关系
先以WebKit为例,WebKit事实上由两部分组成的
- WebCore:负责HTML解析、布局、渲染等等相关的工作
- JavaScriptCore:解析、执行JavaScript代码

小程序中也是这样的划分
- 在小程序中编写的JavaScript代码就是被JSCore执行的

2. 认识V8引擎
官方对V8引擎的定义
V8是用
C++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等它实现ECMAScript和WebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32, ARM或MIPS处理器的Linux系统上运行
V8可以独立运行,也可以嵌入到任何C ++应用程序中
3. V8引擎原理

V8引擎本身的源码非常复杂,大概有超过100w行C++代码,但是可以简单了解一下它执行JavaScript代码的原理
3.1. Parse
Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;
如果函数没有被调用,那么是不会被转换成AST的;
Parse的V8官方文档:https://v8.dev/blog/scanner
包括
词法分析和语法分析在 https://astexplorer.net/ 中输入
const name="Hello",得到词法分析树1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34{
"type": "Program",
"start": 0,
"end": 20,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 20,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 20,
"id": {
"type": "Identifier",
"start": 6,
"end": 10,
"name": "name"
},
"init": {
"type": "Literal",
"start": 13,
"end": 20,
"value": "Hello",
"raw": "\"Hello\""
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
3.2. Ignition
Ignition是一个解释器,会将AST转换成ByteCode(字节码)
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
- 如果函数只调用一次,Ignition会执行解释执行ByteCode;
- 根据
不同的平台生成不同的字节码,而生成的字节码是V8引擎约定好的,是跨平台的 - Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter
3.3. TurboFan
TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;
- 如果一个函数被多次调用,那么就会被标记为
热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能; - 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
- TurboFan的V8官方文档:https://v8.dev/blog/turbofan-jit
3.4. 内存回收
上面是JavaScript代码的执行过程,事实上V8的内存回收也是其强大的另外一个原因:
- Orinoco模块,负责垃圾回收,将程序中不需要的内存回收;
- Orinoco的V8官方文档:https://v8.dev/blog/trash-talk
4. js源码是如何被解析的
v8引擎官方解析图


JavaScript源码是如何被解析(Parse)的?
- Blink内核(Webkit内核分支)遇到script标签时,会下载js源码,并放入数据流(Stream)中传递给v8引擎,Stream会对数据进行编码转换
- Scanner会进行
词法分析(lexical analysis),词法分析会将代码转换成tokens - 接下来tokens会被转换成AST树,经过Parser和PreParser:
- Parser就是直接将token转成AST树架构
- PreParse 称为预解析,为什么需要预解析?
- 这是因为并不是所有的JavaScript代码,在一开始就会被执行。那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率
- 所以v8引擎就实现了
Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行 - 比如在一个函数outer内部定义了另一个函数inner,那么inner函数就会进行预解析
5. JavaScrip代码执行原理-版本说明
在ECMA早期的版本中(ECMAScript3),代码的执行流程的术语和ECMAScript5以及之后的术语会有所区别
- 目前网上大多数流行的说法都是基于ECMAScript3版本的解析,并且在面试时问到的大多数都是ECMAScript3的版本内容
- 但是ECMAScript3终将过去, ECMAScript5必然会成为主流,所以最好也理解ECMAScript5甚至包括ECMAScript6以及更 好版本的内容
- 事实上在TC39( ECMAScript5 )的最新描述中,和ECMAScript5之后的版本又出现了一定的差异
那么如何学习
- 通过ECMAScript3中的概念学习JavaScript执行原理、作用域、作用域链、闭包等概念
- 通过ECMAScript5中的概念学习块级作用域、let、const等概念
事实上,它们只是在对某些概念上的描述不太一样,在整体思路上都是一致的
6. js代码是怎么执行的
1 | var name = 'test' |
6.1. 初始化全局对象
Global Object
- There is a unique global object, which is created before control enters any execution context. Initially the global object has the following properties:
- Built-in objects such as Math, String, Date, parseInt, etc. These have attributes { DontEnum }.
- Additional host defined properties. This may include a property whose value is the global object itself; for example, in the HTML document object model the window property of the global object is the global object itself.
- There is a unique global object, which is created before control enters any execution context. Initially the global object has the following properties:
v8引擎会在解析源代码到AST的过程中,在
堆内存中创建一个全局对象:GlobalObject(GO)该对象所有作用域(scope)都可以访问
- 这个对象会包含浏览器或者node环境下的全局对象:Date,Array,String,Number,setTimeout,setInterval等
其中的
window属性会执行globalobject这个对象本身在解析过程中会把代码加入到 globalobject 这个对象里,并不会给属性赋值,只有当代码被执行时才会赋值,代码被运行前会从磁盘中取出再加入到内存,在内存中转为机器指令后又到cpu中执行
1 | var GlobalObject = { |
6.2. 执行上下文栈
Execution Contexts
- When control is transferred to ECMASCript executable code, control is entering an execution context. Active execution contexts logically form a stack. The top execution context on this logical stack is the running execution context.
内存会被划分为
栈结构和堆结构v8引擎内部会有一个
执行上下文栈(execution contenxt stack,简称ECS),它是用于执行代码的调用栈,代码被执行前需要先放到这个栈结构中它执行的是
全局的代码块- 全局的代码块为了执行会创建一个
全局执行上下文栈(global execution context,GEC) - GEC会被放入到ECS中执行
- 全局的代码块为了执行会创建一个
GEC被放入到ECS中里面包含两部分内容:
- 第一部分:在代码之前,在parser转成AST的过程中,会将全局定义的变量,函数等加入到GlobalObject中,但是并不会赋值
- 这个过程也称为变量的作用域提升(hoisting)
- 第二部分:在代码执行中,对变量赋值,或者执行其他的函数
- 第一部分:在代码之前,在parser转成AST的过程中,会将全局定义的变量,函数等加入到GlobalObject中,但是并不会赋值
6.3. 认识VO对象(Variable Object)
- 每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会添加到这个VO对象中
- Every execution content has associated with it a variable object. Variables and functions declared in the source text are added as properties of the variable object. For function code, parameters are added as properties of the variable object.
- 当全局代码被执行的时候,VO就是GO对象了
- Global object
- The scope chain is created and initialized to contain the global object and no others.
- Variable instantiation is performed using the global object as the variable object and using property attributes { DontDelete }.
- The this value is the global object.
- Global object
6.4. 全局代码执行过程
执行前

执行后

6.5. 函数如何执行
When control enters an execution context for function code, an object called the activation object is created and associated with the execution context. The activation object is initialised with a property with name arguments and attributes { DontDelete }. The initial value of this property is the arguments object descributed below.
The activation object is then used as the variable object for purposes of variable instantiation.
在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到EC Stack中
因为每个执行上下文都会关联一个VO,那么函数执行上下文关联的VO是什么?
- 当进入一个函数执行上下文时,会创建一个AO对象(Activation Object)
- 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数
- 这个AO对象会作为执行上下文的VO来存放变量的初始化
函数执行上下文中包含三部分内容
第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO)
- AO中包含形参、arguments、函数定义和指向函数对象、定义的变量
第二部分:作用域链(scope chain)由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找
第三部分:this绑定的值
6.6. 作用域和作用域链(Scope Chain)
Every execution context has associated with it a scope chain. A scope chain is a list of objects that are searched when evaluating an Identifier. When control enters an execution context, a scope chain is created and populated with an initial set of objects, depending on the type of code. During execution within an execution context, the scope chain of the exectuion context is affected only by with statements and catch clauses.
当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)
- 作用域链是一个对象列表,用于变量标识符的求值
- 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象
函数对象在被创建的那一刻,函数作用域链就被创建了
1
2
3
4
5
6
7
8
9
10var message = "scope chain";
function foo(){
var name = "Hello World";
function bar() {
console.log(name);
}
return bar;
}
var bar = foo();
bar();


6.7. 变量环境和记录
上面的讲解都是基于早期ECMA的版本规范
- Every execution context has associated with it a variable object. Variables and functions declared in the source text are added as properties of the variable object. For function code, parameters are added as properties of the variable object.
- 每一个执行上下文会被关联到一个变量对象,在源代码中的便利和函数声明会被作为属性添加到VO中。对于函数来说,参数也会被添加到VO中
在最新的ECMA的版本规范中,对于一些词汇进行了修改
- Every execution context has an associated VariableEnvironment. Variables and functions declared in ECMAScript code evlauated in an execution context are added as bindings in that VariableEnvironment’s Environment Record. For function code, parameters are also added as bindings to that Environment Record.
- 每一个执行上下文会关联到一个
变量环境(VariableEnvironment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中
通过上面的变化可以知道,在最新的ECMA标注中,前面的变量对象VO已经有另外一个称呼了变量环境VE
7. 作用域提升面试题
7.1. 1
1 | var n = 100 |

7.2. 2
1 | function foo(){ |
在解析foo函数过程中,n会被加入到AO中,只是不会被赋值

7.3. 3
1 | var n = 100 |

7.4. 4
1 | var a = 100 |
在解析foo函数过程中,不论是否存在return,a都会被加入到AO中,只是不会被赋值

7.5. 5
1 | function foo(){ |
