本文的是建立在v8引擎的基础之上,所使用的分析工具是d8。v8是谷歌的js引擎,chrome和大部分浏览器都是以v8为js虚拟机。而d8可以理解v8的调试工具,可以利用d8查看js代码在执行过程各种中间数据,比如生成的ast,字节码,二进制代码等。
js的虚拟机v8,在执行这段代码之前,还是有不少的事情要做的。准备全局执行上下文,包括内置函数全局变量;准备全局作用域,包括全局变量,在执行过程中的所有数据都要放在内存当中; 初始化内存当中的堆栈;初始化消息系统 准备好基础环境之后,v8才会接收我们这段代码。
题中完整的代码是下面这样的,如果没有了解过这道题的朋友,可以思考一下,a.x的结果是什么。
var a = {n:1}
a.x = a = {n:2}
console.log( a.x )
先说答案:undefined
。
在开始分析之前,先简单介绍下v8解释器的架构:字节码,寄存器,栈,堆:
- 字节码是由Ignition解释器生成的,包含了AST和作用域信息。
- 寄存器用来存放中间数据,主要有通用寄存器r0、r1等,用来指向下条要执行的字节码的PC寄存器,有指向栈顶的栈顶寄存器,累加器是一个特殊的寄存器,用来存放中间结果。
- 栈和堆是用来提供指令集,区别是提供的指令集不同。
接收到代码之后,v8会先使用parser解析器解析生成AST,然后使用ignition生成字节码。
以下是v8为上面那段代码生成的字节码:
0000033A0824FAD6 @ 0 : 12 00 LdaConstant [0]
0000033A0824FAD8 @ 2 : 26 fa Star r1
0000033A0824FADA @ 4 : 27 fe f9 Mov <closure>, r2
0000033A0824FADD @ 7 : 61 37 01 fa 02 CallRuntime [DeclareGlobals], r1-r2
0000033A0824FAE2 @ 12 : 7d 01 00 29 CreateObjectLiteral [1], [0], #41
0000033A0824FAE6 @ 16 : 15 02 01 StaGlobal [2], [1]
0000033A0824FAE9 @ 19 : 13 02 03 LdaGlobal [2], [3]
0000033A0824FAEC @ 22 : 26 fa Star r1
0000033A0824FAEE @ 24 : 7d 03 05 29 CreateObjectLiteral [3], [5], #41
0000033A0824FAF2 @ 28 : 15 02 01 StaGlobal [2], [1]
0000033A0824FAF5 @ 31 : 26 f9 Star r2
0000033A0824FAF7 @ 33 : 2d fa 04 06 StaNamedProperty r1, [4], [6]
0000033A0824FAFB @ 37 : 27 f9 fb Mov r2, r0
0000033A0824FAFE @ 40 : 25 fb Ldar r0
0000033A0824FB00 @ 42 : aa Return
接下来,就开始这篇文章吧。
一、先从var a = { n : 1 }
开始
首先要是var a
声明一个标识符为a
的变量,基本概念就不讲了。预解析会将这段代码处理成一个AST树,这个树大概是下面这个样子的。
然后var a = { n : 1 }
这一串代码当中,var a
只是一个在语法分析阶段作为标识符来理解的字面文本,n
和2
本质上也是一个表达式。var
从来不进行求值计算。
000002110824FA82 @ 0 : 12 00 LdaConstant [0]
// 取出常量池中[0]的内容写入累加器中
// 累加器:a
000002110824FA84 @ 2 : 26 fa Star r1
// 把累加器中的内容保存在寄存器r1当中
// 累加器:undefined,r1寄存器:a
000002110824FA86 @ 4 : 27 fe f9 Mov <closure>, r2
// 把寄存器的内容放到r2当中
// 累加器:undefined,r1寄存器:undefined,r2寄存器:a
000002110824FA89 @ 7 : 61 37 01 fa 02 CallRuntime [DeclareGlobals], r1-r2
// 用r1-r2中的内容作为参数调用DeclareGlobals,将a声明为全局变量。
// 此时全局变量栈为[ a : undefined ]。
其次是{ n : 1 }
000002110824FA8E @ 12 : 7e CreateObjectLiteral [1], [0], #41
// 创建一个字面量对象,对象被分配在堆中,用数组下标代表对象的引用的地址,把这个地址写入累加器中。
// 累加器:{n:1},r1寄存器:undefined,r2寄存器:a
再进行=
的赋值操作
000000550824FAE6 @ 16 : 15 02 01 StaGlobal [2], [1]
// 将累加器的内容存到全局变量a里。
// 此时全局变量栈中[2]为 (a : { n : 1 })
2.然后是a.x = a = { n : 2 }
前面的var a = { n : 1 }
应该是不会任何的疑问。
关键的在于下面的a.x = a = { n : 2 }
。
以var x = y = 1
来举个例子,这是一个典型的赋值语句,在语法是将表达式1的值绑定给y,在将y的值绑定给x,而非x的值等于y的值等于1,只有y = 1
才是一个表达式。y
只是一个意外,在js当中,如果向一个不存在的变量进行赋值,那么js会在全局变量中创建这个变量。
倘若将语句修改为const x = y = 1
,之后 x = 2
浏览器会报Assignment to constant variable
错误,但是y = 2
却不会有任何问题。这个语句,可以理解解为y = 1; const x = y
而a.x = a = { n : 2 }
中,是两个连续赋值的表达式,也一样可以理解为a = { n : 2 }; a.x = a
。
这时候可能会有人有疑问,那为什么a.x
的结果是undefined
而不是a
?
在js当中,任何运算的操作都是严格从左到右地进行计算,而a.x
是优先被处理的,这是一个运算,表示a.x
所代表的是{ n : 1 }
对象的内存地址,a.x
的计算结果已经固定为一个引用,假设这里a.x
先获取到的a
对象的内存地址是100000f90
。而后面a = { n : 2 }
则是在堆空间中创建了一个新的对象,这个对象的内存地址为100000f98
。
那么a.x =
所操作的是100000f90
地址的对象,而a =
是将100000f98
对象赋值到a上,并不能影响到a.x
的值,因为a.x
已经是一个计算结果,而非变量。也就是说,a.x
操作的是堆内存中的{ n : 1 }
而非{ n : 2 }
。
在这之后,将100000f98
这个内存地址绑定到的100000f90
所指向的对象的x
属性上去。
var a = { n : 1 }
var ref = a
a.x = a = { n :2 }
// a的值为{ n : 2 },ref的值为{ n : 1, x : { n : 2 } }
a.n = 3
// a的值为{ n : 2 },ref的值为{ n : 1, x : { n : 3 } }
理解了之后,就可以看看v8生成了什么样的字节码,是如何处理的。
读取{ n : 1 }
并存到r1寄存器
000000550824FAE9 @ 19 : 13 02 03 LdaGlobal [2], [3]
// 读取全局变量[2]的内容({ n : 1 }的地址)并储存到累加器中
000000550824FAEC @ 22 : 26 fa Star r1
// 将累加器中的内容存到r1寄存器当中
// 累加器:{ n : 1 },r1寄存器:{ n : 1 }。
创建{ n : 2 }
,修改全局变量栈中a
的地址为该对象,然后将对象存到r2寄存器。
000000550824FAEE @ 24 : 7d 03 05 29 CreateObjectLiteral [3], [5], #41
// 在堆中创建字面量对象{ n : 2 },并将地址存到累加器中。
// 累加器:{ n : 2 },r1寄存器:{ n : 1 }。
000000550824FAF2 @ 28 : 15 02 01 StaGlobal [2], [1]
// 将累加器的内容({ n : 2})存到全局变量栈[2](a)的位置
// 累加器:{ n : 2 },r1寄存器:{ n : 1 }。
000000550824FAF5 @ 31 : 26 f9 Star r2
// 将累加器({ n : 2})的内容存到r2寄存器
// 累加器:undefined,r1寄存器:{ n : 1 },r2寄存器:{ n : 2 }。
将r2寄存器中的内容绑定到{ n : 1 }
的x
属性。从此处也可以知道,a.x
操作是r1寄存器,也就是{ n :1 }
。
000000550824FAF7 @ 33 : 2d fa 04 06 StaNamedProperty r1, [4], [6]
// 将常量池中[4]的值赋给 r1
// 累加器:undefined,r1寄存器:{ n : 1, x : { n : 2 }},r2寄存器:{ n : 2 }。
最后返回值。
000000550824FAFB @ 37 : 27 f9 fb Mov r2, r0
// 将r2寄存器的内容移动到r0寄存器
// r0寄存器:{ n : 2 }
000000550824FAFE @ 40 : 25 fb Ldar r0
// 将寄存器r0的内容转移到累加器
// 累加器:{ n : 2 }
000000550824FB00 @ 42 : aa Return
// 返回累加器中的值({ n : 2 })
至此,这道题的分析就已经完成。
最后,前文提到过语句和表达式的概念,闲着可以,思考一下delete 0
这个表达式的结果。
ECMAScript规定任何表达式计算的结果,要么是值要么是引用,delete 0
这个操作,之所以会返回true
,是因为0
本身就是一个字面值表达式的计算结果,并不代表现实意义上的0,也不可能可以将0从系统中删除,所以,这行代码实际上什么也没有发生。
关于字节码的解释,我也不是很会,只是看了些文章之后尝试解读,如有错误欢迎指出。