JavaScript 代码运行
以大家开发常用的 Chrome 浏览器或 Node 举例,我们的 JavaScript 代码是通过 V8 运行的。但 V8 是怎么执行代码的呢?当我们输入 const foo = {foo:'foo'} 时 V8 又做了什么?笔者先抛出以上问题,我们接着往下看。
JavaScript 存储
在代码运行时,最重要的前提便是有一个能够存储状态的地方,这便是我们所述的堆栈空间。
我们的基础类型是保存在栈中的,会自动进行回收;而复合类型是保存在堆中的,通过GC操作进行空间释放。这一过程对于用户来说是隐式的,因此用户必须按照 JavaScript 的规范来写代码,如果没有符合规范,那 GC 就无法正确的回收空间,因此会造成 ML 现象,更严重的就会造成 OOM。
为了更直观的看清每一种类型在内存中的存储形式,笔者创建了一个基础类型变量 Foo,复合类型 Bar,以及一个声明 John,并给出它们在内存堆栈中的状态图:
通过上述分析,我们提到了 GC 会对无效对象进行回收以及空间释放,对于用户而言,不管是基础类型还是复合类型他们的声明与释放都是自动的。但实际上关于堆的回收是手动的,只是在 V8 层面已经帮我们实现了而已,并且这一过程也不是完全免费的(write barrier)。但这一自动的过程让很大部分开发人可以完全忽视它的存在,显然 JavaScript 是故意设计如此
write barrier 用于在异步三色标记算法进行中通知 GC 目前对象图变更的所有操作,以保证三色标记法在异步过程中的准确性, v8 插入的 write barrier 代码。
// Called after `object.field = value`.
write_barrier(object, field_offset, value) {
if (color(object) == black && color(value) == white) {
set_color(value, grey);
marking_worklist.push(value);
}
}
while(1){
iters++;
if((iters % 500) == 0)
write(1, which_child?"B":"A", 1);
int what = rand() % 23;
if(what == 1){
close(open("grindir/../a", O_CREATE|O_RDWR));
} else if(what == 2){
close(open("grindir/../grindir/../b", O_CREATE|O_RDWR));
} else if(what == 3){
unlink("grindir/../a");
} else if(what == 4){
if(chdir("grindir") != 0){
printf("grind: chdir grindir failed\n");
exit(1);
}
unlink("../b");
chdir("/");
} else if(what == 5){
close(fd);
fd = open("/grindir/../a", O_CREATE|O_RDWR);
} else if(what == 6){
close(fd);
fd = open("/./grindir/./../b", O_CREATE|O_RDWR);
} else if(what == 7){
write(fd, buf, sizeof(buf));
} else if(what == 8){
read(fd, buf, sizeof(buf));
} else if(what == 9){
mkdir("grindir/../a");
close(open("a/../a/./a", O_CREATE|O_RDWR));
unlink("a/a");
} else if(what == 10){
mkdir("/../b");
close(open("grindir/../b/b", O_CREATE|O_RDWR));
unlink("b/b");
} else if(what == 11){
unlink("b");
link("../grindir/./../a", "../b");
} else if(what == 12){
unlink("../grindir/../a");
link(".././b", "/grindir/../a");
} else if(what == 13){
int pid = fork();
if(pid == 0){
exit(0);
} else if(pid < 0){
printf("grind: fork failed\n");
exit(1);
}
wait(0);
} else if(what == 14){
int pid = fork();
if(pid == 0){
fork();
fork();
exit(0);
} else if(pid < 0){
printf("grind: fork failed\n");
V8 编译过程
foo({foo: 1});
function foo(obj) {
const bar = obj.foo + 1
return bar + '1'
}
初始化“堆空间”、“栈空间”
初始化全局上下文环境,包括执行过程中的全局信息,变量等
初始化全局作用域。而函数作用域以及其他子作用域是执行时才存在的
初始化事件循环系统
[generating bytecode for function: foo]
--- AST ---
FUNC at 28
. KIND 0
. LITERAL ID 1
. SUSPEND COUNT 0
. NAME "foo"
. PARAMS
. . VAR (0x7fe5318086d8) (mode = VAR, assigned = false) "obj"
. DECLS
. . VARIABLE (0x7fe5318086d8) (mode = VAR, assigned = false) "obj"
. . VARIABLE (0x7fe531808780) (mode = CONST, assigned = false) "bar"
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 50
. . . INIT at 50
. . . . VAR PROXY local[0] (0x7fe531808780) (mode = CONST, assigned = false) "bar"
. . . . ADD at 58
. . . . . PROPERTY at 54
. . . . . . VAR PROXY parameter[0] (0x7fe5318086d8) (mode = VAR, assigned = false) "obj"
. . . . . . NAME foo
. . . . . LITERAL 1
. RETURN at 67
. . ADD at 78
. . . VAR PROXY local[0] (0x7fe531808780) (mode = CONST, assigned = false) "bar"
. . . LITERAL "1"
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "foo"
},
"params": [
{
"type": "Identifier",
"name": "obj"
}
],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "bar"
},
"init": {
"type": "BinaryExpression",
"left": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "obj"
},
"property": {
"type": "Identifier",
"name": "foo"
},
},
"operator": "+",
"right": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
}
],
},
{
"type": "ReturnStatement",
"start": 51,
"end": 67,
"argument": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "bar"
},
"operator": "+",
"right": {
"type": "Literal",
"value": "1",
"raw": "'1'"
}
}
}
]
}
}
],
}
Global scope:
global { // (0x7f91fb010a48) (0, 51)
// will be compiled
// 1 stack slots
// temporary vars:
TEMPORARY .result; // (0x7f91fb010ef8) local[0]
// local vars:
VAR foo; // (0x7f91fb010e68)
function foo () { // (0x7f91fb010ca8) (20, 51)
// lazily parsed
// 2 heap slots
}
}
Global scope:
function foo () { // (0x7f91fb010c60) (20, 51)
// will be compiled
}
由于 JavaScript 字节码目前并没有和 JVM 或 ESTree 那样标准化,因此其格式会与 V8 引擎版本紧密相关。
很多的字节码都满足以下正则 /^(Lda|Sta).+$/ 它们当中的 a 代指累加器 (accumulator),主要用于描述把值操作到累加器寄存器中,或把当前在累加器中的值取出并存储在寄存器中。因此可以把解释器理解成是带有累加器的寄存器
[generated bytecode for function: foo (0x3a50082d25cd <SharedFunctionInfo foo>)]
Bytecode length: 14
Parameter count 2
Register count 1
Frame size 8
OSR nesting level: 0
Bytecode Age: 0
0x3a50082d278e @ 0 : 28 03 00 01 LdaNamedProperty a0, [0], [1]
0x3a50082d2792 @ 4 : 41 01 00 AddSmi [1], [0]
0x3a50082d2795 @ 7 : c6 Star0
0x3a50082d2796 @ 8 : 12 01 LdaConstant [1]
0x3a50082d2798 @ 10 : 35 fa 03 Add r0, [3]
0x3a50082d279b @ 13 : ab Return
Constant pool (size = 2)
0x3a50082d275d: [FixedArray] in OldSpace
- map: 0x3a5008042205 <Map>
- length: 2
0: 0x3a50082d2535 <String[3]: #foo>
1: 0x3a500804494d <String[1]: #1>
Handler Table (size = 0)
Source Position Table (size = 0)
JavaScript Object
原始类型:原始类型主要包括:null、undefined、boolean、number、string、bigint、symbol,以类似栈数据结构存储,遵循先进后出的原则,而且具有 immutable 特点,比如我们修改了 string 的值,V8 会返回给我们一个全新的 string。
对象类型:JavaScript 是建立在对象之上的语言,所以对象的属性值自然也可以是另一个对象。
函数类型:如果函数作为对象的属性,我们一般称其为方法。
function foo(obj) {
const bar = obj.foo + 1
return bar + '1'
}
foo({foo: 1});
在 ES6 出现模块作用域之前,JavaScript 中没有私有作用域的概念,因此在多人开发项目的时候,常常会使用单例模式,以 IIFE 的模式创建一个 namespace 以减少全局变量命名冲突的问题。因此 IIFE 最大的特点是执行不会污染环境,函数和函数内部的变量都不会被其他部分的代码访问到,外部只能获取到 IIFE 的返回结果。
(function foo(obj) {
const bar = obj.foo + 1
return bar + '1'
})({foo: 1})
name 属性造就被浏览器广泛支持,但是直到 ES6 才将其写入标准,ES6 之前的 name 属性之所以可以获取到函数名称,是因为 V8 对外暴露了相应的接口。Function 构造函数返回的函数实例,name 属性的值为 anonymous
(new Function).name // "anonymous"
code 属性表示的是函数编码,以 string 的形式存储在内存中。当执行到一个函数调用语句时,V8 会从函数对象中取出 code 属性值,然后解释执行这段函数代码。V8 没有对外暴露 code 属性,因此无法直接输出。
function Foo() {
this["bar3"] = 'bar-3'
this[10] = 'foo-10'
this[1] = 'foo-1'
this["bar1"] = 'bar-1'
this[10000] = 'foo-10000'
this[3] = 'foo-3'
this[0] = 'foo-0'
this["bar2"] = 'bar-2'
}
const foo = new Foo()
for(key in bar){
console.log(`key: ${key} value:${foo[item]}`)
}
key: 0 value:foo-0
key: 1 value:foo-1
key: 3 value:foo-3
key: 10 value:foo-10
key: 10000 value:foo-10000
key: bar3 value:bar-3
key: bar1 value:bar-1
key: bar2 value:bar-2
key 为数字的属性被优先打印,并升序排列。
key 为字符串的属性按照被定义时的顺序进行排列。
对象内 key 为数字的属性称为 elements(排序属性),此类属性通过浪费空间换取时间,直接下标访问,提升访问速度。当 element 的序号十分不连续时,会优化成为 hash 表。
对象内 key 为字符串的属性称为 properties(常规属性),通过把对象的属性和值分成线性数据结构和属性字典结构后,以优化原本的完全字典存储。properties 属性默认采用链表结构,当数据量很小时,查找也会很快,但数据量上升到某个数值后,会优化成为 hash 表。上述对象在内存中存储如图所示:
对象内属性是可以动态扩充的。The number of in-object properties is predetermined by the initial size of the object。但笔者目前没有见到对象内属性通过动态扩容大于 10 个的情况。
function Foo(_elementsNum, _propertiesNum) {
// set elements
for (let i = 0; i < _elementsNum; i++) {
this[i] = `element${i}`;
}
// set property
for (let i = 0; i < _propertiesNum; i++) {
let ppt = `property${i}`;
this[ppt] = ppt + 'value';
}
}
const foos = new Foo(100, 100);
const foos = new Foo(10000, 10000
[marking 0x2ca4082d26e5 <JSFunction Foo (sfi = 0x2ca4082d25f5)> for optimized recompilation, reason: small function]
[compiling method 0x2ca4082d26e5 <JSFunction Foo (sfi = 0x2ca4082d25f5)> (target TURBOFAN) using TurboFan OSR]
[optimizing 0x2ca4082d26e5 <JSFunction Foo (sfi = 0x2ca4082d25f5)> (target TURBOFAN) - took 1.135, 3.040, 0.287 ms]
[marking 0x2ca4082d26e5 <JSFunction Foo (sfi = 0x2ca4082d25f5)> for optimized recompilation, reason: small function]
[compiling method 0x2ca4082d26e5 <JSFunction Foo (sfi = 0x2ca4082d25f5)> (target TURBOFAN) using TurboFan OSR]
[optimizing 0x2ca4082d26e5 <JSFunction Foo (sfi = 0x2ca4082d25f5)> (target TURBOFAN) - took 0.596, 1.681, 0.050 ms]
JavaScript 类型系统
In[2]: 1+'1'
Traceback (most recent call last):
File "..", line 1, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-2-0cdad81f9201>", line 1, in <module>
1+'1'
TypeError: unsupported operand type(s) for +: 'int' and 'str'
console.log(1+'1')
// 11
在维基百科中,类型系统是这样定义的:在计算机科学中,类型系统(type system)用于定义如何将编程语言中的数值和表达式归类为许多不同的类型,如何操作这些类型,这些类型如何互相作用。类型可以确认一个值或者一组值具有特定的意义和目的(虽然某些类型,如抽象类型和函数类型,在程序运行中,可能不表示为值)。类型系统在各种语言之间有非常大的不同,也许,最主要的差异存在于编译时期的语法,以及运行时期的操作实现方式。
1.Let lref be the result of evaluating AdditiveExpression.
2.Let lval be GetValue(lref).
3.ReturnIfAbrupt(lval).
4.Let rref be the result of evaluating MultiplicativeExpression.
5.Let rval be GetValue(rref).
6.ReturnIfAbrupt(rval).
7.Let lprim be ToPrimitive(lval).
8.ReturnIfAbrupt(lprim).
9.Let rprim be ToPrimitive(rval).
10.ReturnIfAbrupt(rprim).
11.If Type(lprim) is String or Type(rprim) is String, then
a.Let lstr be ToString(lprim).
b.ReturnIfAbrupt(lstr).
c.Let rstr be ToString(rprim).
d.ReturnIfAbrupt(rstr).
e.Return the String that is the result of concatenating lstr and rstr.
12.Let lnum be ToNumber(lprim).
13.ReturnIfAbrupt(lnum).
14.Let rnum be ToNumber(rprim).
15.ReturnIfAbrupt(rnum).
16.Return the result of applying the addition operation to lnum and rnum. See the Note below
const foo = 1 + '1' + null + undefined + 1n
// 表达式被 V8 转换为
const foo = Number(1).toString() + '1' + String(null) + String(undefined) + BigInt(1n).toString()
// "11nullundefined1"
type NumberOrString = number | string
type PrototypeFunction<T> = (input: Record<string, any>, flag:T) => T
type ToPrimitive = PrototypeFunction<NumberOrString>
如果 object 为基本类型,直接返回结果
否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
否则,JavaScript 抛出一个 TypeError 异常。
如果 object 为基本类型,直接返回结果
否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
否则,JavaScript 抛出一个 TypeError 异常。
/*
例一
*/
{ foo: 'foo' } + { bar: 'bar' }
// "[object Object][object Object]"
/*
例二
*/
{
foo: 'foo',
valueOf() {
return 'foo';
},
toString() {
return 'bar';
},
} +
{
bar: 'bar',
toString() {
return 'bar';
},
}
// "foobar"
/*
例三
*/
{
foo: 'foo',
valueOf() {
return Object.create(null);
},
toString() {
return Object.create(null);
},
} +
{
bar: 'bar',
}
// Uncaught TypeError: Cannot convert object to primitive value
/*
例四
*/
const date = new Date();
date.valueof = () => '123';
date.toString = () => '456';
date + 1;
// "4561"
总结
利用 V8 深入理解 JavaScript,这个标题可能起的有点狂,但对于笔者来说通过对此学习确实更进一步理解了 JavaScript 甚至其他语言的工作机制,同时对前端和技术栈等概念有了更深层次的思考。
本文主要通过日常简单的代码存储引出V8相关以及计算机科学的一些概念,从JavaScript 的定位推导出当前设计的原因,以及结合 V8 工作流程给出一个宏观的认识;接着通过详细的步骤完整的展现了 V8 编译流水线每个环节的产物;通过分析 JavaScript 对象引出其存储规则;最后通过类型系统引出 V8 对不同类型数据进行交互的规则实现。
对于 V8 庞大而复杂的执行结构来说本文只阐述了凤毛麟角,文中有太多的话题可以用来延伸引出更多值得研究的学问,希望同学们通过本文可以有所收获和思考,如果文中有错误欢迎在评论区指出。
袋鼠云在大数据领域深耕7年,拥有丰富的大数据平台建设经验和成熟的产品体系,想了解或咨询更多有关袋鼠云大数据产品、行业解决方案、客户案例的朋友,浏览袋鼠云官网:https://www.dtstack.com/?src=bbs
同时,欢迎对大数据开源项目有兴趣的同学加入「袋鼠云开源框架钉钉技术群」,交流最新开源技术信息,群号码:30537511,项目地址:https://github.com/DTStack