转载信息

V8 是由 Google 开发的开源 JavaScript 引擎,也被称为虚拟机,模拟实际计算机各种功能来实现代码的编译和执行。
我们写的 JavaScript 代码直接交给浏览器或者 Node 执行时,底层的 CPU 是不认识的,也没法执行。CPU 只认识自己的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情。并且不同类型的 CPU 的指令集是不一样的,那就意味着需要给每一种 CPU 重写汇编代码。 JavaScirpt 引擎可以将 JS 代码编译为不同 CPU(Intel, ARM 以及 MIPS 等)对应的汇编代码,这样我们就不需要去翻阅每个 CPU 的指令集手册来编写汇编代码了。当然,JavaScript 引擎的工作也不只是编译代码,它还要负责执行代码、分配内存以及垃圾回收。
# 将一个寄存器中的数据移动到另外一个寄存器中
1000100111011000 #机器指令
mov ax,bx #汇编指令
资料拓展: 汇编语言入门教程
Google V8 引擎是用 C ++编写的开源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等。可以运行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上。 V8 最早被开发用以嵌入到 Google 的开源浏览器 Chrome 中,第一个版本随着第一版Chrome于 2008 年 9 月 2 日发布。但是 V8 是一个可以独立运行的模块,完全可以嵌入到任何 C ++应用程序中。著名的 Node.js( 一个异步的服务器框架,可以在服务端使用 JavaScript 写出高效的网络服务器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。
和其他 JavaScript 引擎一样,V8 会编译 / 执行 JavaScript 代码,管理内存,负责垃圾回收,与宿主语言的交互等。通过暴露宿主对象 ( 变量,函数等 ) 到 JavaScript,JavaScript 可以访问宿主环境中的对象,并在脚本中完成对宿主对象的操作。

资料拓展:v8 logo | V8 (JavaScript engine) | 《V8、JavaScript+的现在与未来》 | 几张图让你看懂 WebAssembly
V8 一词最早见于“V-8 engine”,即V8 发动机,一般使用在中高端车辆上。8 个气缸分成两组,每组 4 个,成 V 型排列。是高层次汽车运动中最常见的发动机结构,尤其在美国,IRL,ChampCar 和 NASCAR 都要求使用 V8 发动机。
d8 是一个非常有用的调试工具,你可以把它看成是 debug for V8 的缩写。我们可以使用 d8 来查看 V8 在执行 JavaScript 过程中的各种中间数据,比如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还可以使用 d8 提供的私有 API 查看一些内部信息。
V8 源码编译出来的可执行程序名为 d8。d8 作为 V8 引擎在命令行中可以使用的交互 shell 存在。Google 官方已经不记得 d8 这个名字的由来,但是做为”delveloper shell”的缩写,用首字母 d 和 8 结合,恰到好处。 还有一种说法是 d8 最初叫
developer shell,因为 d 后面有 8 个字符,因此简写为 d8,类似于i18n(internationalization)这样的简写。 参考:Using d8
方法一:自行下载编译
方法二:使用编译好的 d8 工具
// 解压文件,点击d8打开(mac安全策略限制的话,按住control,再点击,弹出菜单中选择打开)
V8 version 8.4.109
d8> 1 + 2
3
d8> 2 + '4'
"24"
d8> console.log(23)
23
undefined
d8> var a = 1
undefined
d8> a + 2
3
d8> this
[object global]
d8>
本文后续用于 demo 演示时的文件目录结构:
V8:
# d8可执行文件
d8
icudtl.dat
libc++.dylib
libchrome_zlib.dylib
libicui18n.dylib
libicuuc.dylib
libv8.dylib
libv8_debug_helper.dylib
libv8_for_testing.dylib
libv8_libbase.dylib
libv8_libplatform.dylib
obj
snapshot_blob.bin
v8_build_config.json
# 新建的js示例文件
test.js
方法三:mac
# 如果已有HomeBrew,忽略第一条命令
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install v8
方法四:使用 node 代替,比如可以用node --print-bytecode ./test.js,打印出 Ignition(解释器)生成的 Bytecode(字节码)。
查看 d8 命令
# 如果不想使用./d8这种方式进行调试,可将d8加入环境变量,之后就可以直接`d8 --help`了
./d8 --help
过滤特定的命令
# 如果是 Windows 系统,可能缺少 grep 程序,请自行下载安装并添加环境变量
./d8 --help |grep print
如:
function sum(a) {
var b = 6;
return a + 6;
}
console.log(sum(3));
# d8 后面跟上文件名和要执行的命令,如执行下面这行命令,就会打印出 test.js 文件所生成的字节码。
./d8 ./test.js --print-bytecode
# 执行以下命令,输出9
./d8 ./test.js
你还可以使用 V8 所提供的一些内部方法,只需要在启动 V8 时传入 --allow-natives-syntax 命令,你就可以在 test.js 中使用诸如HasFastProperties(检查一个对象是否拥有快属性)的内部方法。
function Foo(property_num, element_num) {
//添加可索引属性
for (let i = 0; i < element_num; i++) {
this[i] = `element${i}`;
}
//添加常规属性
for (let i = 0; i < property_num; i++) {
let ppt = `property${i}`;
this[ppt] = ppt;
}
}
var bar = new Foo(10, 10);
// 检查一个对象是否拥有快属性
console.log(%HasFastProperties(bar));
delete bar.property2;
console.log(%HasFastProperties(bar));
./d8 --allow-natives-syntax ./test.js
# 依次打印:true false

V8 是一个非常复杂的项目,有超过 100 万行 C++代码。它由许多子模块构成,其中这 4 个模块是最重要的:
Parser:负责将 JavaScript 源码转换为 Abstract Syntax Tree (AST)
确切的说,在“Parser”将 JavaScript 源码转换为 AST 前,还有一个叫”Scanner“的过程,具体流程如下:
Ignition:interpreter,即解释器,负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型;解释器执行时主要有四个模块,内存中的字节码、寄存器、栈、堆。
通常有两种类型的解释器,基于栈 (Stack-based)和基于寄存器 (Register-based),基于栈的解释器使用栈来保存函数参数、中间运算结果、变量等;基于寄存器的虚拟机则支持寄存器的指令操作,使用寄存器来保存参数、中间计算结果。通常,基于栈的虚拟机也定义了少量的寄存器,基于寄存器的虚拟机也有堆栈,其区别体现在它们提供的指令集体系。大多数解释器都是基于栈的,比如 Java 虚拟机,.Net 虚拟机,还有早期的 V8 虚拟机。基于堆栈的虚拟机在处理函数调用、解决递归问题和切换上下文时简单明快。而现在的 V8 虚拟机则采用了基于寄存器的设计,它将一些中间数据保存到寄存器中。 基于寄存器的解释器架构:
资料参考:解释器是如何解释执行字节码的?
TurboFan:compiler,即编译器,利用 Ignition 所收集的类型信息,将 Bytecode 转换为优化的汇编代码;
Orinoco:garbage collector,垃圾回收模块,负责将程序不再需要的内存空间回收。
其中,Parser,Ignition 以及 TurboFan 可以将 JS 源码编译为汇编代码,其流程图如下:

简单地说,Parser 将 JS 源码转换为 AST,然后 Ignition 将 AST 转换为 Bytecode,最后 TurboFan 将 Bytecode 转换为经过优化的 Machine Code(实际上是汇编代码)。
图片中的红色虚线是逆向的,也就是说 Optimized Machine Code 会被还原为 Bytecode,这个过程叫做 Deoptimization。这是因为 Ignition 收集的信息可能是错误的,比如 add 函数的参数之前是整数,后来又变成了字符串。生成的 Optimized Machine Code 已经假定 add 函数的参数是整数,那当然是错误的,于是需要进行 Deoptimization。
function add(x, y) {
return x + y;
}
add(1, 2);
add("1", "2");
在运行 C、C++以及 Java 等程序之前,需要进行编译,不能直接执行源码;但对于 JavaScript 来说,我们可以直接执行源码(比如:node test.js),它是在运行的时候先编译再执行,这种方式被称为即时编译(Just-in-time compilation),简称为 JIT。因此,V8 也属于 JIT 编译器。
资料拓展参考:V8 引擎是如何工作的?

V8 本质上是一个虚拟机,因为计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种手段:
解释执行和编译执行都有各自的优缺点,解释执行启动速度快,但是执行时速度慢,而编译执行启动速度慢,但是执行速度快。为了充分地利用解释执行和编译执行的优点,规避其缺点,V8 采用了一种权衡策略,在启动过程中采用了解释执行的策略,但是如果某段代码的执行频率超过一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码。
总结:
V8 执行一段 JavaScript 代码所经历的主要流程包括:
// 闭包(静态作用域,一等公民,调用栈的矛盾体)
function foo() {
var d = 20;
return function inner(a, b) {
const c = a + b + d;
return c;
};
}
const f = foo();
关于闭包,可参考我以前的一篇文章,在此不再赘述,在此主要谈下闭包给 Chrome V8 带来的问题及其解决策略。
所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。
V8 引入预解析器,比如当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该函数做一次快速的预解析。
下面的代码会输出什么:
function Foo() {
this[200] = "test-200";
this[1] = "test-1";
this[100] = "test-100";
this["B"] = "bar-B";
this[50] = "test-50";
this[9] = "test-9";
this[8] = "test-8";
this[3] = "test-3";
this[5] = "test-5";
this["D"] = "bar-D";
this["C"] = "bar-C";
}
var bar = new Foo();
for (key in bar) {
console.log(`index:${key} value:${bar[key]}`);
}
//输出:
// index:1 value:test-1
// index:3 value:test-3
// index:5 value:test-5
// index:8 value:test-8
// index:9 value:test-9
// index:50 value:test-50
// index:100 value:test-100
// index:200 value:test-200
// index:B value:bar-B
// index:D value:bar-D
// index:C value:bar-C
在 ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。在这里我们把对象中的数字属性称为排序属性,在 V8 中被称为 elements,字符串属性就被称为常规属性,在 V8 中被称为 properties。在 V8 内部,为了有效地提升存储和访问这两种属性的性能,分别使用了两个线性数据结构来分别保存排序属性和常规属性。同时 v8 将部分常规属性直接存储到对象本身,我们把这称为对象内属性 (in-object properties),不过对象内属性的数量是固定的,默认是 10 个。
function Foo(property_num, element_num) {
//添加可索引属性
for (let i = 0; i < element_num; i++) {
this[i] = `element${i}`;
}
//添加常规属性
for (let i = 0; i < property_num; i++) {
let ppt = `property${i}`;
this[ppt] = ppt;
}
}
var bar = new Foo(10, 10);
可以通过 Chrome 开发者工具的 Memory 标签,捕获查看当前的内存快照。通过增大第一个参数来查看存储变化。(Console 面板运行以上代码,打开 Memory 面板,通过点击Take heap snapshot记录内存快照,点击快照,筛选出 Foo 进行查看。可参考使用 chrome-devtools Memory 面板了解 Memory 面板。)
我们将保存在线性数据结构中的属性称之为“快属性”,因为线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除大量的属性时,则执行效率会非常低,这主要因为会产生大量时间和内存开销。因此,如果一个对象的属性过多时,V8 就会采取另外一种存储策略,那就是“慢属性”策略,但慢属性的对象内部会有独立的非线性数据结构 (字典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。
v8 属性存储:

总结:
因为 JavaScript 中的对象是由一组组属性和值组成的,所以最简单的方式是使用一个字典来保存属性和值,但是由于字典是非线性结构,所以如果使用字典,读取效率会大大降低。为了提升查找效率,V8 在对象中添加了两个隐藏属性,排序属性和常规属性,element 属性指向了 elements 对象,在 elements 对象中,会按照顺序存放排序属性。properties 属性则指向了 properties 对象,在 properties 对象中,会按照创建时的顺序保存常规属性。
通过引入这两个属性,加速了 V8 查找属性的速度,为了更加进一步提升查找效率,V8 还实现了内置内属性的策略,当常规属性少于一定数量时,V8 就会将这些常规属性直接写进对象中,这样又节省了一个中间步骤。
但是如果对象中的属性过多时,或者存在反复添加或者删除属性的操作,那么 V8 就会将线性的存储模式降级为非线性的字典存储模式,这样虽然降低了查找速度,但是却提升了修改对象的属性的速度。
function factorial(n) {
if (n === 1) {
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(50000));
继承就是一个对象可以访问另外一个对象中的属性和方法,在 JavaScript 中,我们通过原型和原型链的方式来实现了继承特性。
JavaScript 的每个对象都包含了一个隐藏属性 __proto__ ,我们就把该隐藏属性 __proto__ 称之为该对象的原型 (prototype),__proto__ 指向了内存中的另外一个对象,我们就把 __proto__ 指向的对象称为该对象的原型对象,那么该对象就可以直接访问其原型对象的方法或者属性。
JavaScript 中的继承非常简洁,就是每个对象都有一个原型属性,该属性指向了原型对象,查找属性的时候,JavaScript 虚拟机会沿着原型一层一层向上查找,直至找到正确的属性。
__proto__var animal = {
type: "Default",
color: "Default",
getInfo: function () {
return `Type is: ${this.type},color is ${this.color}.`;
},
};
var dog = {
type: "Dog",
color: "Black",
};
利用__proto__实现继承:
dog.__proto__ = animal;
dog.getInfo();
通常隐藏属性是不能使用 JavaScript 来直接与之交互的。虽然现代浏览器都开了一个口子,让 JavaScript 可以访问隐藏属性 __proto__,但是在实际项目中,我们不应该直接通过 __proto__ 来访问或者修改该属性,其主要原因有两个:
__proto__会直接破坏现有已经优化的结构,触发 V8 重构该对象的隐藏类!在 JavaScript 中,使用 new 加上构造函数的这种组合来创建对象和实现对象的继承。不过使用这种方式隐含的语义过于隐晦。其实是 JavaScript 为了吸引 Java 程序员、在语法层面去蹭 Java 热点,所以就被硬生生地强制加入了非常不协调的关键字 new。
function DogFactory(type, color) {
this.type = type;
this.color = color;
}
var dog = new DogFactory("Dog", "Black");
其实当 V8 执行上面这段代码时,V8 在背后悄悄地做了以下几件事情:
var dog = {};
dog.__proto__ = DogFactory.prototype;
DogFactory.call(dog, "Dog", "Black");
早期的 V8 为了提升代码的执行速度,直接将 JavaScript 源代码编译成了没有优化的二进制机器代码,如果某一段二进制代码执行频率过高,那么 V8 会将其标记为热点代码,热点代码会被优化编译器优化,优化后的机器代码执行效率更高。
随着移动设备的普及,V8 团队逐渐发现将 JavaScript 源码直接编译成二进制代码存在两个致命的问题:
这两个问题无疑会阻碍 V8 在移动设备上的普及,于是 V8 团队大规模重构代码,引入了中间的字节码。字节码的优势有如下三点:
Bytecode 某种程度上就是汇编语言,只是它没有对应特定的 CPU,或者说它对应的是虚拟的 CPU。这样的话,生成 Bytecode 时简单很多,无需为不同的 CPU 生产不同的代码。要知道,V8 支持 9 种不同的 CPU,引入一个中间层 Bytecode,可以简化 V8 的编译流程,提高可扩展性。
如果我们在不同硬件上去生成 Bytecode,会发现生成代码的指令是一样的。
function add(x, y) {
var z = x + y;
return z;
}
console.log(add(1, 2));
运行./d8 ./test.js --print-bytecode:
[generated bytecode for function: add (0x01000824fe59 <SharedFunctionInfo add>)]
Parameter count 3 #三个参数,包括了显式地传入的 x 和 y,还有一个隐式地传入的 this
Register count 1
Frame size 8
0x10008250026 @ 0 : 25 02 Ldar a1 #将a1寄存器中的值加载到累加器中,LoaD Accumulator from Register
0x10008250028 @ 2 : 34 03 00 Add a0, [0]
0x1000825002b @ 5 : 26 fb Star r0 #Store Accumulator to Register,把累加器中的值保存到r0寄存器中
0x1000825002d @ 7 : aa Return #结束当前函数的执行,并将控制权传回给调用方
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)
3
常用字节码指令:
Add a0, [0]是从 a0 寄存器加载值并将其与累加器中的值相加,然后将结果再次放入累加器。
add a0 后面的[0]称之为 feedback vector slot,又叫反馈向量槽,它是一个数组,解释器将解释执行过程中的一些数据类型的分析信息都保存在这个反馈向量槽中了,目的是为了给 TurboFan 优化编译器提供优化信息,很多字节码都会为反馈向量槽提供运行时信息。

JavaScript 是一门动态语言,其执行效率要低于静态语言,V8 为了提升 JavaScript 的执行速度,借鉴了很多静态语言的特性,比如实现了 JIT 机制,为了提升对象的属性访问速度而引入了隐藏类,为了加速运算而引入了内联缓存。
静态语言中,如 C++ 在声明一个对象之前需要定义该对象的结构,代码在执行之前需要先被编译,编译的时候,每个对象的形状都是固定的,也就是说,在代码的执行过程中是无法被改变的。可以直接通过偏移量查询来查询对象的属性值,这也就是静态语言的执行效率高的一个原因。
JavaScript 在运行时,对象的属性是可以被修改的,所以当 V8 使用了一个对象时,比如使用了 obj.x 的时候,它并不知道该对象中是否有 x,也不知道 x 相对于对象的偏移量是多少,也就是说 V8 并不知道该对象的具体的形状。那么,当在 JavaScript 中要查询对象 obj 中的 x 属性时,V8 会按照具体的规则一步一步来查询,这个过程非常的慢且耗时。
let point1 = { x: 100, y: 200 };
let point2 = { x: 200, y: 300 };
let point3 = (({ x: 100 } % DebugPrint(point1)) % DebugPrint(point2)) % DebugPrint(point3);
./d8 --allow-natives-syntax ./test.js
# ===============
DebugPrint: 0x1ea3080c5bc5: [JS_OBJECT_TYPE]
# V8 为 point1 对象创建的隐藏类
- map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1ea3080406e9 <FixedArray[0]> {
#x: 100 (const data field 0)
#y: 200 (const data field 1)
}
0x1ea308284ce9: [Map]
- type: JS_OBJECT_TYPE
- instance size: 20
- inobject properties: 2
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
- instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
- dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
# ===============
DebugPrint: 0x1ea3080c5c1d: [JS_OBJECT_TYPE]
# V8 为 point2 对象创建的隐藏类
- map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1ea3080406e9 <FixedArray[0]> {
#x: 200 (const data field 0)
#y: 300 (const data field 1)
}
0x1ea308284ce9: [Map]
- type: JS_OBJECT_TYPE
- instance size: 20
- inobject properties: 2
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
- instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
- dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
# ===============
DebugPrint: 0x1ea3080c5c31: [JS_OBJECT_TYPE]
# V8 为 point3 对象创建的隐藏类
- map: 0x1ea308284d39 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1ea3080406e9 <FixedArray[0]> {
#x: 100 (const data field 0)
}
0x1ea308284d39: [Map]
- type: JS_OBJECT_TYPE
- instance size: 16
- inobject properties: 1
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1ea308284d11 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
- instance descriptors (own) #1: 0x1ea3080c5c41 <DescriptorArray[1]>
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
- dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
let point = {} % DebugPrint(point);
point.x = 100 % DebugPrint(point);
point.y = 200 % DebugPrint(point);
# ./d8 --allow-natives-syntax ./test.js
DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
- map: 0x32c7082802d9 <Map(HOLEY_ELEMENTS)> [FastProperties]
...
DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
- map: 0x32c708284cc1 <Map(HOLEY_ELEMENTS)> [FastProperties]
...
DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
- map: 0x32c708284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
...
虽然隐藏类能够加速查找对象的速度,但是在 V8 查找对象属性值的过程中,依然有查找对象的隐藏类和根据隐藏类来查找对象属性值的过程。如果一个函数中利用了对象的属性,并且这个函数会被多次执行:
function loadX(obj) {
return obj.x;
}
var obj = { x: 1, y: 3 };
var obj1 = { x: 3, y: 6 };
var obj2 = { x: 3, y: 6, z: 8 };
for (var i = 0; i < 90000; i++) {
// 对比时间差异
console.time(`---${i}----`);
loadX(obj);
console.timeEnd(`---${i}----`);
loadX(obj1);
// 产生多态
loadX(obj2);
}
通常 V8 获取 obj.x 的流程:
内联缓存及其原理:
单态、多态和超态:
总结: V8 引入了内联缓存(IC),IC 会监听每个函数的执行过程,并在一些关键的地方埋下监听点,这些包括了加载对象属性 (Load)、给对象属性赋值 (Store)、还有函数调用 (Call),V8 会将监听到的数据写入一个称为反馈向量 (FeedBack Vector) 的结构中,同时 V8 会为每个执行的函数维护一个反馈向量。有了反馈向量缓存的临时数据,V8 就可以缩短对象属性的查找路径,从而提升执行效率。但是针对函数中的同一段代码,如果对象的隐藏类是不同的,那么反馈向量也会记录这些不同的隐藏类,这就出现了多态和超态的情况。我们在实际项目中,要尽量避免出现多态或者超态的情况。
回调函数有两种类型:同步回调和异步回调,同步回调函数是在执行函数内部被执行的,而异步回调函数是在执行函数外部被执行的。 通用 UI 线程宏观架构:

UI 线程提供一个消息队列,并将待执行的事件添加到消息队列中,然后 UI 线程会不断循环地从消息队列中取出事件、执行事件。关于异步回调,这里也有两种不同的类型,其典型代表是 setTimeout 和 XMLHttpRequest:
调用栈:调用栈是一种数据结构,用来管理在主线程上执行的函数的调用关系。主线程在执行任务的过程中,如果函数的调用层次过深,可能造成栈溢出的错误,我们可以使用 setTimeout 来解决栈溢出的问题。setTimeout 的本质是将同步函数调用改成异步函数调用,这里的异步调用是将回调函数封装成宏任务,并将其添加进消息队列中,然后主线程再按照一定规则循环地从消息队列中读取下一个宏任务。
宏任务:就是指消息队列中的等待被主线程执行的事件。每个宏任务在执行时,V8 都会重新创建栈,然后随着宏任务中函数调用,栈也随之变化,最终,当该宏任务执行结束时,整个栈又会被清空,接着主线程继续执行下一个宏任务。
微任务:你可以把微任务看成是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
JavaScript 中之所以要引入微任务,主要是由于主线程执行消息队列中宏任务的时间颗粒度太粗了,无法胜任一些对精度和实时性要求较高的场景,微任务可以在实时性和效率之间做一个有效的权衡。另外使用微任务,可以改变我们现在的异步编程模型,使得我们可以使用同步形式的代码来编写异步调用。
微任务是基于消息队列、事件循环、UI 主线程还有堆栈而来的,然后基于微任务,又可以延伸出协程、Promise、Generator、await/async 等现代前端经常使用的一些技术。
// 不会使浏览器卡死
function foo() {
setTimeout(foo, 0);
}
foo();
微任务:
// 浏览器console控制台可使浏览器卡死(无法响应鼠标事件等)
function foo() {
return Promise.resolve().then(foo);
}
foo();

协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。比如,当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。每一时刻,该线程只能执行其中某一个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
资料拓展:co 函数库的含义和用法
从“GC Roots”对象出发,遍历 GC Root 中的所有对象,如果通过 GC Roots 没有遍历到的对象,则这些对象便是垃圾数据。V8 会有专门的垃圾回收器来回收这些垃圾数据。
垃圾回收大致可以分为以下几个步骤:
第一步,通过 GC Root 标记空间中活动对象和非活动对象。目前 V8 采用的可访问性(reachability)算法来判断堆中的对象是否是活动对象。具体地讲,这个算法是将一些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中的所有对象:
第二步,回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
第三步,做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大的连续内存时,就有可能出现内存不足的情况,所以最后一步需要整理这些内存碎片。但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片(比如副垃圾回收器)。
由于 JavaScript 是运行在主线程之上的,因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
资料参考:深入解读 V8 引擎的「并发标记」技术
Daniel Clifford 在 Google I/O 2012 上做了一个精彩的演讲“Breaking the JavaScript Speed Limit with V8”。在演讲中,他深入解释了 13 个简单的代码优化方法,可以让你的 JavaScript 代码在 Chrome V8 引擎编译/运行时更加快速。在演讲中,他介绍了怎么优化,并解释了原因。下面简明的列出了13 个 JavaScript 性能提升技巧:
try{} catch{}(如果存在 try/catch 代码快,则将性能敏感的代码放到一个嵌套的函数中);演讲资料参考: Performance Tips for JavaScript in V8 | 译文 | 内网视频 | YouTube
资料参考:How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code | 译文
box 操作参考:JavaScript 类型:关于类型,有哪些你不知道的细节? | JavaScript 的装箱和拆箱 | 谈谈 JavaScript 中装箱和拆箱
资料参考: JavaScript Start-up Performance | JavaScript 启动性能瓶颈分析与解决方案
转载信息

V8 是由 Google 开发的开源 JavaScript 引擎,也被称为虚拟机,模拟实际计算机各种功能来实现代码的编译和执行。
我们写的 JavaScript 代码直接交给浏览器或者 Node 执行时,底层的 CPU 是不认识的,也没法执行。CPU 只认识自己的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情。并且不同类型的 CPU 的指令集是不一样的,那就意味着需要给每一种 CPU 重写汇编代码。 JavaScirpt 引擎可以将 JS 代码编译为不同 CPU(Intel, ARM 以及 MIPS 等)对应的汇编代码,这样我们就不需要去翻阅每个 CPU 的指令集手册来编写汇编代码了。当然,JavaScript 引擎的工作也不只是编译代码,它还要负责执行代码、分配内存以及垃圾回收。
# 将一个寄存器中的数据移动到另外一个寄存器中
1000100111011000 #机器指令
mov ax,bx #汇编指令
资料拓展: 汇编语言入门教程
Google V8 引擎是用 C ++编写的开源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等。可以运行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上。 V8 最早被开发用以嵌入到 Google 的开源浏览器 Chrome 中,第一个版本随着第一版Chrome于 2008 年 9 月 2 日发布。但是 V8 是一个可以独立运行的模块,完全可以嵌入到任何 C ++应用程序中。著名的 Node.js( 一个异步的服务器框架,可以在服务端使用 JavaScript 写出高效的网络服务器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。
和其他 JavaScript 引擎一样,V8 会编译 / 执行 JavaScript 代码,管理内存,负责垃圾回收,与宿主语言的交互等。通过暴露宿主对象 ( 变量,函数等 ) 到 JavaScript,JavaScript 可以访问宿主环境中的对象,并在脚本中完成对宿主对象的操作。

资料拓展:v8 logo | V8 (JavaScript engine) | 《V8、JavaScript+的现在与未来》 | 几张图让你看懂 WebAssembly
V8 一词最早见于“V-8 engine”,即V8 发动机,一般使用在中高端车辆上。8 个气缸分成两组,每组 4 个,成 V 型排列。是高层次汽车运动中最常见的发动机结构,尤其在美国,IRL,ChampCar 和 NASCAR 都要求使用 V8 发动机。
d8 是一个非常有用的调试工具,你可以把它看成是 debug for V8 的缩写。我们可以使用 d8 来查看 V8 在执行 JavaScript 过程中的各种中间数据,比如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还可以使用 d8 提供的私有 API 查看一些内部信息。
V8 源码编译出来的可执行程序名为 d8。d8 作为 V8 引擎在命令行中可以使用的交互 shell 存在。Google 官方已经不记得 d8 这个名字的由来,但是做为”delveloper shell”的缩写,用首字母 d 和 8 结合,恰到好处。 还有一种说法是 d8 最初叫
developer shell,因为 d 后面有 8 个字符,因此简写为 d8,类似于i18n(internationalization)这样的简写。 参考:Using d8
方法一:自行下载编译
方法二:使用编译好的 d8 工具
// 解压文件,点击d8打开(mac安全策略限制的话,按住control,再点击,弹出菜单中选择打开)
V8 version 8.4.109
d8> 1 + 2
3
d8> 2 + '4'
"24"
d8> console.log(23)
23
undefined
d8> var a = 1
undefined
d8> a + 2
3
d8> this
[object global]
d8>
本文后续用于 demo 演示时的文件目录结构:
V8:
# d8可执行文件
d8
icudtl.dat
libc++.dylib
libchrome_zlib.dylib
libicui18n.dylib
libicuuc.dylib
libv8.dylib
libv8_debug_helper.dylib
libv8_for_testing.dylib
libv8_libbase.dylib
libv8_libplatform.dylib
obj
snapshot_blob.bin
v8_build_config.json
# 新建的js示例文件
test.js
方法三:mac
# 如果已有HomeBrew,忽略第一条命令
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install v8
方法四:使用 node 代替,比如可以用node --print-bytecode ./test.js,打印出 Ignition(解释器)生成的 Bytecode(字节码)。
查看 d8 命令
# 如果不想使用./d8这种方式进行调试,可将d8加入环境变量,之后就可以直接`d8 --help`了
./d8 --help
过滤特定的命令
# 如果是 Windows 系统,可能缺少 grep 程序,请自行下载安装并添加环境变量
./d8 --help |grep print
如:
function sum(a) {
var b = 6;
return a + 6;
}
console.log(sum(3));
# d8 后面跟上文件名和要执行的命令,如执行下面这行命令,就会打印出 test.js 文件所生成的字节码。
./d8 ./test.js --print-bytecode
# 执行以下命令,输出9
./d8 ./test.js
你还可以使用 V8 所提供的一些内部方法,只需要在启动 V8 时传入 --allow-natives-syntax 命令,你就可以在 test.js 中使用诸如HasFastProperties(检查一个对象是否拥有快属性)的内部方法。
function Foo(property_num, element_num) {
//添加可索引属性
for (let i = 0; i < element_num; i++) {
this[i] = `element${i}`;
}
//添加常规属性
for (let i = 0; i < property_num; i++) {
let ppt = `property${i}`;
this[ppt] = ppt;
}
}
var bar = new Foo(10, 10);
// 检查一个对象是否拥有快属性
console.log(%HasFastProperties(bar));
delete bar.property2;
console.log(%HasFastProperties(bar));
./d8 --allow-natives-syntax ./test.js
# 依次打印:true false

V8 是一个非常复杂的项目,有超过 100 万行 C++代码。它由许多子模块构成,其中这 4 个模块是最重要的:
Parser:负责将 JavaScript 源码转换为 Abstract Syntax Tree (AST)
确切的说,在“Parser”将 JavaScript 源码转换为 AST 前,还有一个叫”Scanner“的过程,具体流程如下:
Ignition:interpreter,即解释器,负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型;解释器执行时主要有四个模块,内存中的字节码、寄存器、栈、堆。
通常有两种类型的解释器,基于栈 (Stack-based)和基于寄存器 (Register-based),基于栈的解释器使用栈来保存函数参数、中间运算结果、变量等;基于寄存器的虚拟机则支持寄存器的指令操作,使用寄存器来保存参数、中间计算结果。通常,基于栈的虚拟机也定义了少量的寄存器,基于寄存器的虚拟机也有堆栈,其区别体现在它们提供的指令集体系。大多数解释器都是基于栈的,比如 Java 虚拟机,.Net 虚拟机,还有早期的 V8 虚拟机。基于堆栈的虚拟机在处理函数调用、解决递归问题和切换上下文时简单明快。而现在的 V8 虚拟机则采用了基于寄存器的设计,它将一些中间数据保存到寄存器中。 基于寄存器的解释器架构:
资料参考:解释器是如何解释执行字节码的?
TurboFan:compiler,即编译器,利用 Ignition 所收集的类型信息,将 Bytecode 转换为优化的汇编代码;
Orinoco:garbage collector,垃圾回收模块,负责将程序不再需要的内存空间回收。
其中,Parser,Ignition 以及 TurboFan 可以将 JS 源码编译为汇编代码,其流程图如下:

简单地说,Parser 将 JS 源码转换为 AST,然后 Ignition 将 AST 转换为 Bytecode,最后 TurboFan 将 Bytecode 转换为经过优化的 Machine Code(实际上是汇编代码)。
图片中的红色虚线是逆向的,也就是说 Optimized Machine Code 会被还原为 Bytecode,这个过程叫做 Deoptimization。这是因为 Ignition 收集的信息可能是错误的,比如 add 函数的参数之前是整数,后来又变成了字符串。生成的 Optimized Machine Code 已经假定 add 函数的参数是整数,那当然是错误的,于是需要进行 Deoptimization。
function add(x, y) {
return x + y;
}
add(1, 2);
add("1", "2");
在运行 C、C++以及 Java 等程序之前,需要进行编译,不能直接执行源码;但对于 JavaScript 来说,我们可以直接执行源码(比如:node test.js),它是在运行的时候先编译再执行,这种方式被称为即时编译(Just-in-time compilation),简称为 JIT。因此,V8 也属于 JIT 编译器。
资料拓展参考:V8 引擎是如何工作的?

V8 本质上是一个虚拟机,因为计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种手段:
解释执行和编译执行都有各自的优缺点,解释执行启动速度快,但是执行时速度慢,而编译执行启动速度慢,但是执行速度快。为了充分地利用解释执行和编译执行的优点,规避其缺点,V8 采用了一种权衡策略,在启动过程中采用了解释执行的策略,但是如果某段代码的执行频率超过一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码。
总结:
V8 执行一段 JavaScript 代码所经历的主要流程包括:
// 闭包(静态作用域,一等公民,调用栈的矛盾体)
function foo() {
var d = 20;
return function inner(a, b) {
const c = a + b + d;
return c;
};
}
const f = foo();
关于闭包,可参考我以前的一篇文章,在此不再赘述,在此主要谈下闭包给 Chrome V8 带来的问题及其解决策略。
所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。
V8 引入预解析器,比如当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该函数做一次快速的预解析。
下面的代码会输出什么:
function Foo() {
this[200] = "test-200";
this[1] = "test-1";
this[100] = "test-100";
this["B"] = "bar-B";
this[50] = "test-50";
this[9] = "test-9";
this[8] = "test-8";
this[3] = "test-3";
this[5] = "test-5";
this["D"] = "bar-D";
this["C"] = "bar-C";
}
var bar = new Foo();
for (key in bar) {
console.log(`index:${key} value:${bar[key]}`);
}
//输出:
// index:1 value:test-1
// index:3 value:test-3
// index:5 value:test-5
// index:8 value:test-8
// index:9 value:test-9
// index:50 value:test-50
// index:100 value:test-100
// index:200 value:test-200
// index:B value:bar-B
// index:D value:bar-D
// index:C value:bar-C
在 ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。在这里我们把对象中的数字属性称为排序属性,在 V8 中被称为 elements,字符串属性就被称为常规属性,在 V8 中被称为 properties。在 V8 内部,为了有效地提升存储和访问这两种属性的性能,分别使用了两个线性数据结构来分别保存排序属性和常规属性。同时 v8 将部分常规属性直接存储到对象本身,我们把这称为对象内属性 (in-object properties),不过对象内属性的数量是固定的,默认是 10 个。
function Foo(property_num, element_num) {
//添加可索引属性
for (let i = 0; i < element_num; i++) {
this[i] = `element${i}`;
}
//添加常规属性
for (let i = 0; i < property_num; i++) {
let ppt = `property${i}`;
this[ppt] = ppt;
}
}
var bar = new Foo(10, 10);
可以通过 Chrome 开发者工具的 Memory 标签,捕获查看当前的内存快照。通过增大第一个参数来查看存储变化。(Console 面板运行以上代码,打开 Memory 面板,通过点击Take heap snapshot记录内存快照,点击快照,筛选出 Foo 进行查看。可参考使用 chrome-devtools Memory 面板了解 Memory 面板。)
我们将保存在线性数据结构中的属性称之为“快属性”,因为线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除大量的属性时,则执行效率会非常低,这主要因为会产生大量时间和内存开销。因此,如果一个对象的属性过多时,V8 就会采取另外一种存储策略,那就是“慢属性”策略,但慢属性的对象内部会有独立的非线性数据结构 (字典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。
v8 属性存储:

总结:
因为 JavaScript 中的对象是由一组组属性和值组成的,所以最简单的方式是使用一个字典来保存属性和值,但是由于字典是非线性结构,所以如果使用字典,读取效率会大大降低。为了提升查找效率,V8 在对象中添加了两个隐藏属性,排序属性和常规属性,element 属性指向了 elements 对象,在 elements 对象中,会按照顺序存放排序属性。properties 属性则指向了 properties 对象,在 properties 对象中,会按照创建时的顺序保存常规属性。
通过引入这两个属性,加速了 V8 查找属性的速度,为了更加进一步提升查找效率,V8 还实现了内置内属性的策略,当常规属性少于一定数量时,V8 就会将这些常规属性直接写进对象中,这样又节省了一个中间步骤。
但是如果对象中的属性过多时,或者存在反复添加或者删除属性的操作,那么 V8 就会将线性的存储模式降级为非线性的字典存储模式,这样虽然降低了查找速度,但是却提升了修改对象的属性的速度。
function factorial(n) {
if (n === 1) {
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(50000));
继承就是一个对象可以访问另外一个对象中的属性和方法,在 JavaScript 中,我们通过原型和原型链的方式来实现了继承特性。
JavaScript 的每个对象都包含了一个隐藏属性 __proto__ ,我们就把该隐藏属性 __proto__ 称之为该对象的原型 (prototype),__proto__ 指向了内存中的另外一个对象,我们就把 __proto__ 指向的对象称为该对象的原型对象,那么该对象就可以直接访问其原型对象的方法或者属性。
JavaScript 中的继承非常简洁,就是每个对象都有一个原型属性,该属性指向了原型对象,查找属性的时候,JavaScript 虚拟机会沿着原型一层一层向上查找,直至找到正确的属性。
__proto__var animal = {
type: "Default",
color: "Default",
getInfo: function () {
return `Type is: ${this.type},color is ${this.color}.`;
},
};
var dog = {
type: "Dog",
color: "Black",
};
利用__proto__实现继承:
dog.__proto__ = animal;
dog.getInfo();
通常隐藏属性是不能使用 JavaScript 来直接与之交互的。虽然现代浏览器都开了一个口子,让 JavaScript 可以访问隐藏属性 __proto__,但是在实际项目中,我们不应该直接通过 __proto__ 来访问或者修改该属性,其主要原因有两个:
__proto__会直接破坏现有已经优化的结构,触发 V8 重构该对象的隐藏类!在 JavaScript 中,使用 new 加上构造函数的这种组合来创建对象和实现对象的继承。不过使用这种方式隐含的语义过于隐晦。其实是 JavaScript 为了吸引 Java 程序员、在语法层面去蹭 Java 热点,所以就被硬生生地强制加入了非常不协调的关键字 new。
function DogFactory(type, color) {
this.type = type;
this.color = color;
}
var dog = new DogFactory("Dog", "Black");
其实当 V8 执行上面这段代码时,V8 在背后悄悄地做了以下几件事情:
var dog = {};
dog.__proto__ = DogFactory.prototype;
DogFactory.call(dog, "Dog", "Black");
早期的 V8 为了提升代码的执行速度,直接将 JavaScript 源代码编译成了没有优化的二进制机器代码,如果某一段二进制代码执行频率过高,那么 V8 会将其标记为热点代码,热点代码会被优化编译器优化,优化后的机器代码执行效率更高。
随着移动设备的普及,V8 团队逐渐发现将 JavaScript 源码直接编译成二进制代码存在两个致命的问题:
这两个问题无疑会阻碍 V8 在移动设备上的普及,于是 V8 团队大规模重构代码,引入了中间的字节码。字节码的优势有如下三点:
Bytecode 某种程度上就是汇编语言,只是它没有对应特定的 CPU,或者说它对应的是虚拟的 CPU。这样的话,生成 Bytecode 时简单很多,无需为不同的 CPU 生产不同的代码。要知道,V8 支持 9 种不同的 CPU,引入一个中间层 Bytecode,可以简化 V8 的编译流程,提高可扩展性。
如果我们在不同硬件上去生成 Bytecode,会发现生成代码的指令是一样的。
function add(x, y) {
var z = x + y;
return z;
}
console.log(add(1, 2));
运行./d8 ./test.js --print-bytecode:
[generated bytecode for function: add (0x01000824fe59 <SharedFunctionInfo add>)]
Parameter count 3 #三个参数,包括了显式地传入的 x 和 y,还有一个隐式地传入的 this
Register count 1
Frame size 8
0x10008250026 @ 0 : 25 02 Ldar a1 #将a1寄存器中的值加载到累加器中,LoaD Accumulator from Register
0x10008250028 @ 2 : 34 03 00 Add a0, [0]
0x1000825002b @ 5 : 26 fb Star r0 #Store Accumulator to Register,把累加器中的值保存到r0寄存器中
0x1000825002d @ 7 : aa Return #结束当前函数的执行,并将控制权传回给调用方
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)
3
常用字节码指令:
Add a0, [0]是从 a0 寄存器加载值并将其与累加器中的值相加,然后将结果再次放入累加器。
add a0 后面的[0]称之为 feedback vector slot,又叫反馈向量槽,它是一个数组,解释器将解释执行过程中的一些数据类型的分析信息都保存在这个反馈向量槽中了,目的是为了给 TurboFan 优化编译器提供优化信息,很多字节码都会为反馈向量槽提供运行时信息。

JavaScript 是一门动态语言,其执行效率要低于静态语言,V8 为了提升 JavaScript 的执行速度,借鉴了很多静态语言的特性,比如实现了 JIT 机制,为了提升对象的属性访问速度而引入了隐藏类,为了加速运算而引入了内联缓存。
静态语言中,如 C++ 在声明一个对象之前需要定义该对象的结构,代码在执行之前需要先被编译,编译的时候,每个对象的形状都是固定的,也就是说,在代码的执行过程中是无法被改变的。可以直接通过偏移量查询来查询对象的属性值,这也就是静态语言的执行效率高的一个原因。
JavaScript 在运行时,对象的属性是可以被修改的,所以当 V8 使用了一个对象时,比如使用了 obj.x 的时候,它并不知道该对象中是否有 x,也不知道 x 相对于对象的偏移量是多少,也就是说 V8 并不知道该对象的具体的形状。那么,当在 JavaScript 中要查询对象 obj 中的 x 属性时,V8 会按照具体的规则一步一步来查询,这个过程非常的慢且耗时。
let point1 = { x: 100, y: 200 };
let point2 = { x: 200, y: 300 };
let point3 = (({ x: 100 } % DebugPrint(point1)) % DebugPrint(point2)) % DebugPrint(point3);
./d8 --allow-natives-syntax ./test.js
# ===============
DebugPrint: 0x1ea3080c5bc5: [JS_OBJECT_TYPE]
# V8 为 point1 对象创建的隐藏类
- map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1ea3080406e9 <FixedArray[0]> {
#x: 100 (const data field 0)
#y: 200 (const data field 1)
}
0x1ea308284ce9: [Map]
- type: JS_OBJECT_TYPE
- instance size: 20
- inobject properties: 2
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
- instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
- dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
# ===============
DebugPrint: 0x1ea3080c5c1d: [JS_OBJECT_TYPE]
# V8 为 point2 对象创建的隐藏类
- map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1ea3080406e9 <FixedArray[0]> {
#x: 200 (const data field 0)
#y: 300 (const data field 1)
}
0x1ea308284ce9: [Map]
- type: JS_OBJECT_TYPE
- instance size: 20
- inobject properties: 2
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
- instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
- dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
# ===============
DebugPrint: 0x1ea3080c5c31: [JS_OBJECT_TYPE]
# V8 为 point3 对象创建的隐藏类
- map: 0x1ea308284d39 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1ea3080406e9 <FixedArray[0]> {
#x: 100 (const data field 0)
}
0x1ea308284d39: [Map]
- type: JS_OBJECT_TYPE
- instance size: 16
- inobject properties: 1
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1ea308284d11 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
- instance descriptors (own) #1: 0x1ea3080c5c41 <DescriptorArray[1]>
- prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
- constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
- dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
let point = {} % DebugPrint(point);
point.x = 100 % DebugPrint(point);
point.y = 200 % DebugPrint(point);
# ./d8 --allow-natives-syntax ./test.js
DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
- map: 0x32c7082802d9 <Map(HOLEY_ELEMENTS)> [FastProperties]
...
DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
- map: 0x32c708284cc1 <Map(HOLEY_ELEMENTS)> [FastProperties]
...
DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
- map: 0x32c708284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
...
虽然隐藏类能够加速查找对象的速度,但是在 V8 查找对象属性值的过程中,依然有查找对象的隐藏类和根据隐藏类来查找对象属性值的过程。如果一个函数中利用了对象的属性,并且这个函数会被多次执行:
function loadX(obj) {
return obj.x;
}
var obj = { x: 1, y: 3 };
var obj1 = { x: 3, y: 6 };
var obj2 = { x: 3, y: 6, z: 8 };
for (var i = 0; i < 90000; i++) {
// 对比时间差异
console.time(`---${i}----`);
loadX(obj);
console.timeEnd(`---${i}----`);
loadX(obj1);
// 产生多态
loadX(obj2);
}
通常 V8 获取 obj.x 的流程:
内联缓存及其原理:
单态、多态和超态:
总结: V8 引入了内联缓存(IC),IC 会监听每个函数的执行过程,并在一些关键的地方埋下监听点,这些包括了加载对象属性 (Load)、给对象属性赋值 (Store)、还有函数调用 (Call),V8 会将监听到的数据写入一个称为反馈向量 (FeedBack Vector) 的结构中,同时 V8 会为每个执行的函数维护一个反馈向量。有了反馈向量缓存的临时数据,V8 就可以缩短对象属性的查找路径,从而提升执行效率。但是针对函数中的同一段代码,如果对象的隐藏类是不同的,那么反馈向量也会记录这些不同的隐藏类,这就出现了多态和超态的情况。我们在实际项目中,要尽量避免出现多态或者超态的情况。
回调函数有两种类型:同步回调和异步回调,同步回调函数是在执行函数内部被执行的,而异步回调函数是在执行函数外部被执行的。 通用 UI 线程宏观架构:

UI 线程提供一个消息队列,并将待执行的事件添加到消息队列中,然后 UI 线程会不断循环地从消息队列中取出事件、执行事件。关于异步回调,这里也有两种不同的类型,其典型代表是 setTimeout 和 XMLHttpRequest:
调用栈:调用栈是一种数据结构,用来管理在主线程上执行的函数的调用关系。主线程在执行任务的过程中,如果函数的调用层次过深,可能造成栈溢出的错误,我们可以使用 setTimeout 来解决栈溢出的问题。setTimeout 的本质是将同步函数调用改成异步函数调用,这里的异步调用是将回调函数封装成宏任务,并将其添加进消息队列中,然后主线程再按照一定规则循环地从消息队列中读取下一个宏任务。
宏任务:就是指消息队列中的等待被主线程执行的事件。每个宏任务在执行时,V8 都会重新创建栈,然后随着宏任务中函数调用,栈也随之变化,最终,当该宏任务执行结束时,整个栈又会被清空,接着主线程继续执行下一个宏任务。
微任务:你可以把微任务看成是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
JavaScript 中之所以要引入微任务,主要是由于主线程执行消息队列中宏任务的时间颗粒度太粗了,无法胜任一些对精度和实时性要求较高的场景,微任务可以在实时性和效率之间做一个有效的权衡。另外使用微任务,可以改变我们现在的异步编程模型,使得我们可以使用同步形式的代码来编写异步调用。
微任务是基于消息队列、事件循环、UI 主线程还有堆栈而来的,然后基于微任务,又可以延伸出协程、Promise、Generator、await/async 等现代前端经常使用的一些技术。
// 不会使浏览器卡死
function foo() {
setTimeout(foo, 0);
}
foo();
微任务:
// 浏览器console控制台可使浏览器卡死(无法响应鼠标事件等)
function foo() {
return Promise.resolve().then(foo);
}
foo();

协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。比如,当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。每一时刻,该线程只能执行其中某一个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
资料拓展:co 函数库的含义和用法
从“GC Roots”对象出发,遍历 GC Root 中的所有对象,如果通过 GC Roots 没有遍历到的对象,则这些对象便是垃圾数据。V8 会有专门的垃圾回收器来回收这些垃圾数据。
垃圾回收大致可以分为以下几个步骤:
第一步,通过 GC Root 标记空间中活动对象和非活动对象。目前 V8 采用的可访问性(reachability)算法来判断堆中的对象是否是活动对象。具体地讲,这个算法是将一些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中的所有对象:
第二步,回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
第三步,做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大的连续内存时,就有可能出现内存不足的情况,所以最后一步需要整理这些内存碎片。但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片(比如副垃圾回收器)。
由于 JavaScript 是运行在主线程之上的,因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
资料参考:深入解读 V8 引擎的「并发标记」技术
Daniel Clifford 在 Google I/O 2012 上做了一个精彩的演讲“Breaking the JavaScript Speed Limit with V8”。在演讲中,他深入解释了 13 个简单的代码优化方法,可以让你的 JavaScript 代码在 Chrome V8 引擎编译/运行时更加快速。在演讲中,他介绍了怎么优化,并解释了原因。下面简明的列出了13 个 JavaScript 性能提升技巧:
try{} catch{}(如果存在 try/catch 代码快,则将性能敏感的代码放到一个嵌套的函数中);演讲资料参考: Performance Tips for JavaScript in V8 | 译文 | 内网视频 | YouTube
资料参考:How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code | 译文
box 操作参考:JavaScript 类型:关于类型,有哪些你不知道的细节? | JavaScript 的装箱和拆箱 | 谈谈 JavaScript 中装箱和拆箱
资料参考: JavaScript Start-up Performance | JavaScript 启动性能瓶颈分析与解决方案