一. 深入v8引擎原理
1. js代码的执行
js代码下载好之后,是如何一步步被执行的呢?js代码最终转换成二进制代码,才能被cpu执行
我们知道,浏览器内核是由两部分组成的,以
webkit为例:WebCore**:**负责HTML解析、布局、渲染等相关的工作JavaScriptCore**:**解析、执行js代码
另外一个强大的
JavaScript引擎就是V8引擎
2. v8 引擎的执行原理
- 我们来看一下官方对V8引擎的定义:
V8是用C++编写 的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等它实现
ECMAScript和WebAssembly,并在Windows7或更高版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理器的Linux系统上运行V8可以独立运行,也可以嵌入到任何C ++应用程序中
cpu无法识别js代码,必须转换成二进制代码才能被cpu执行- 先对
js代码进行解析,生成AST抽象语法树(对代码的一个解析,知道代码的结构和含义) lgnition将抽象语法树,转换成字节码(字节码可以跨平台运行)
- 先对
3. v8 引擎的架构
V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对js执行的:Parse模块会将js代码转换成AST(抽象语法树),这是因为解释器并不直接认识js代码- 如果函数没有被调用,那么是不会被转换成
AST的 Parse的V8官方文档:https://v8.dev/blog/scanner
- 如果函数没有被调用,那么是不会被转换成
Ignition是一个解释器,会将AST转换成ByteCode(字节码)- 同时会收集
TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算) - 如果函数只调用一次,
Ignition会解释执行ByteCode Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter
- 同时会收集
TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过
TurboFan转换成优化的机器码,提高代码的执行性能 - 但是,机器码实际上也会被还原为
ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码 TurboFan的V8官方文档:https://v8.dev/blog/turbofan-jit
- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过
4. v8引擎的解析图(官方)

- 词法分析(英文
lexical analysis)- 将字符序列转换成
token序列的过程 token是记号化(tokenization)的缩写
- 将字符序列转换成
- 词法分析器(
lexical analyzer,简称lexer),也叫扫描器(scanne) - 语法分析(英语:
syntactic analysis,也叫parsing)- 语法分析器也可以称之为
parser
- 语法分析器也可以称之为
5. v8引擎的解析图

6. v8引擎执行js代码的大致流程
Parse模块- 将
js代码转成AST Tree(抽象语法树)
- 将
Ignition解释器- 将
AST Tree转换为byte Code(字节码) - 同时收集
TurboFan优化需要的信息
- 将
TurboFan编译器- 将字节码编译为
CPU可以直接执行的机器码(machine code) - 如果某一个函数被多次调用,则会被标记为热点函数,会经过
TurBoFan转换的优化的机器码,让CPU执行,提高代码性能(后续直接使用机器码无需转换) - 如果后续执行代码过程中 改函数调用时的参数类型发生了改变 则会逆向的转成字节码 让
CPU执行
- 将字节码编译为
- 首先会编译
JS,编译过程分为三步:- 词法分析(
scanner)- 会将对应的每一行的代码的字节流分解成有意义的代码块,代码块被称为词法单元(
token进行记号化)
- 会将对应的每一行的代码的字节流分解成有意义的代码块,代码块被称为词法单元(
- 语法分析(
parser)- 将对应的
tokens分析成一个元素逐级嵌套的树 这个树称之为 抽象语法树(Abstract Syntax Tree AST) - 这里也有对应的
pre-parser
- 将对应的
- 将
AST通过Ignition解释器转换成对应的字节码(ByteCode) 交给CPU执行 同时收集类型信息- 将可优化的信息 通过
TurBoFan编译器 编译成更好使用的机器码交给CPU执行 - 如果后续代码的参数类型发生改变 则会逆优化(
Deoptimization)为字节码
- 将可优化的信息 通过
- 词法分析(
二. JS执行上下文
1. JS代码执行原理 - 版本说明
- 在
ECMA早期的版本中(es3),代码的执行流程的术语和es5以及之后的术语会有所区别:- 目前网上大多数流行的说法都是基于
es3版本的解析,并且在面试时问到的大多数都是es3的版本内容 - 但是
es3终将过去,es5必然会成为主流,所以最好也理解es5甚至包括es6以及更好版本的内容 - 事实上在
TC39( es5 )的最新描述中,和es5之后的版本又出现了一定的差异
- 目前网上大多数流行的说法都是基于
- 那么我们按照如下顺序学习:
- 通过
es3中的概念学习js执行原理、作用域、作用域链、闭包等概念 - 通过
es5中的概念学习块级作用域、let、const等概念
- 通过
- 事实上,它们只是在对某些概念上的描述不太一样,在整体思路上都是一致的
2. js的执行过程
假如我们有下面一段代码,它在
js中是如何被执行的呢?jsvar name = 'why' function foo() { var name = 'foo' console.log(name) } var num1 = 20 var num2 = 30 var result = num1 + num2 console.log(result) foo()
3. 初始化全局对象(GO)
js引擎会在执行代码之前,会在堆内存中创建一个全局对象(window):Global Object(GO)- 该对象可以被所有的作用域(
scope)可以访问 - 里面会包含
Date、Array、String、Number、setTimeout、setInterval等 - 其中还有一个
window属性指向自己
- 该对象可以被所有的作用域(

4. 执行上下文( Execution Contexts )
js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈那么现在它要执行谁呢?执行的是全局的代码块:
- 全局的代码块为了执行会创建一个全局执行上下文
Global Execution Context(GEC) GEC会被放入到ECS中执行
- 全局的代码块为了执行会创建一个全局执行上下文
GEC被放入到ECS中里面包含两部分:- 第一部分:在代码执行前,在
parse转成AST的过程中,会将全局定义的变量、函数等加入到GO中,但是变量并不会赋值- 这个过程也称之为变量的作用域提升,这些变量会作为属性添加到当前
VO中,但运行前不会赋值 - 函数声明会提前创建出来,作为属性添加到当前
VO对象中,JS引擎是为了可以提前对函数进行调用,这个过程称之为函数提升
- 这个过程也称之为变量的作用域提升,这些变量会作为属性添加到当前
- 第二部分:在代码执行中,对变量赋值或执行其他的函数

- 第一部分:在代码执行前,在
三. 全局代码执行过程
1. 认识VO对象(Variable Object)
每一个执行上下文会关联一个
VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中
- 每个执行上下文都关联一个变量对象。
- 在代码中声明的变量和函数被添加为变量对象
VO的属性 - 对于执行函数代码,函数的参数被添加为变量对象
AO的属性
当全局代码被执行的时候,
VO就是GO对象了
- 创建并初始化作用域链以包含全局对象而不包含其他对象
- 变量实例化使用全局对象作为变量对象并使用属性来执行
this的值是全局对象
2. 全局代码执行过程(执行前)

3. 全局代码执行过程(执行后)

四. 函数代码执行过程
1. 函数如何被执行呢?
在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(
Functional Execution Context,简称FEC),并且压入到ECS中- 函数定义不会创建函数执行上下文和
AO对象,只有执行函数时,才会创建FEC和AO - 函数执行完成,其对应的
FEC会被弹出ECS,该上下文关联的的AO对象是否被回收,取决于内存是否回收(该对象是否还有可达性、引用) - 同一个函数多次执行,每次执行创建的函数执行上下文,都会创建一个新的
AO对象
- 函数定义不会创建函数执行上下文和
因为每个执行上下文都会关联一个
VO,那么函数执行上下文关联的VO是什么呢?- 当进入一个函数执行上下文时,会创建一个
AO对象(Activation Object),并与该执行上下文关联 - 这个
AO对象会使用arguments作为初始化,并且初始值是传入的参数 - 这个
AO对象会作为执行上下文的VO来存放变量的初始化

- 当进入一个函数执行上下文时,会创建一个
2. 函数的执行过程(执行前)

3. 函数的执行过程(执行后)

五. 作用域和作用域链
当进入到一个执行上下文时,执行上下文也会关联一个作用域链(
Scope Chain)- 作用域链是一个对象列表,用于变量标识符的求值
- 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象
- 这个作用域链对象并赋值给执行上下文中的
scope chain - 查找变量的时候优先查找自己
VO对象中的变量,没有再去自己执行上下文中的scope chain中查找
- 这个作用域链对象并赋值给执行上下文中的
- 函数的作用域链在代码被解析的时候(函数定义的时候),就确定下来了,跟函数怎么调用没有关系


jsvar msg = 'global msg' function foo() { console.log(msg) } var obj = { name: 'obj', bar: function() { var msg = 'bar msg' foo() } } obj.bar() // 'global msg' 函数的作用域链在函数定义的时候就已经被确定下来了
六. 不带标识符定义变量的区别
- 不带
var标识符定义变量,不会产生变量提升(即解析的时候,不会提前添加到当前上下文所关联的VO对象中) - 实际是给
window对象添加了一个属性,相当于一个全局的变量
js
console.log(msg) // msg未定义
msg = 'global msg' // 这种写法其实是给window添加了一个msg属性,赋值为'global msg'
console.log(msg === window.msg) // true
function foo() {
console.log(msg2) // msg2未定义
msg2 = 'foo msg'
console.log(msg2 === window.msg2) // true
}
foo()