前言
在這一篇裡面,我們會來看一些比較經典的例子,藉由分析 bytecode 來重新建造原本的程式碼,反推出在 V8 眼中的執行順序為何。
這邊我挑的例子基本上都是 bytecode 可以看出來的,而許多也很經典的問題例如說 this,像這種光看 bytecode 什麼都看不出來,因此不會被放進題目裡面。
看到底下這些題目以後,大家也可以先想一下答案會是什麼。
經典案例:變數宣告
function find_me_test() {
(function find_me_inner() {
var a = b = 5
})()
console.log(b) // 輸出是什麼?
console.log(a) // 輸出是什麼?
}
find_me_test()
產生的 bytecode:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 24
21 E> 0x1cc4f6c1dd92 @ 0 : a5 StackCheck
28 S> 0x1cc4f6c1dd93 @ 1 : 81 00 00 02 CreateClosure [0], [0], #2
0x1cc4f6c1dd97 @ 5 : 26 fb Star r0
78 E> 0x1cc4f6c1dd99 @ 7 : 5c fb 01 CallUndefinedReceiver0 r0, [1]
83 S> 0x1cc4f6c1dd9c @ 10 : 13 01 03 LdaGlobal [1], [3]
0x1cc4f6c1dd9f @ 13 : 26 fa Star r1
91 E> 0x1cc4f6c1dda1 @ 15 : 28 fa 02 05 LdaNamedProperty r1, [2], [5]
0x1cc4f6c1dda5 @ 19 : 26 fb Star r0
95 E> 0x1cc4f6c1dda7 @ 21 : 13 03 07 LdaGlobal [3], [7]
0x1cc4f6c1ddaa @ 24 : 26 f9 Star r2
91 E> 0x1cc4f6c1ddac @ 26 : 59 fb fa f9 09 CallProperty1 r0, r1, r2, [9]
100 S> 0x1cc4f6c1ddb1 @ 31 : 13 01 03 LdaGlobal [1], [3]
0x1cc4f6c1ddb4 @ 34 : 26 fa Star r1
108 E> 0x1cc4f6c1ddb6 @ 36 : 28 fa 02 05 LdaNamedProperty r1, [2], [5]
0x1cc4f6c1ddba @ 40 : 26 fb Star r0
112 E> 0x1cc4f6c1ddbc @ 42 : 13 04 0b LdaGlobal [4], [11]
0x1cc4f6c1ddbf @ 45 : 26 f9 Star r2
108 E> 0x1cc4f6c1ddc1 @ 47 : 59 fb fa f9 0d CallProperty1 r0, r1, r2, [13]
0x1cc4f6c1ddc6 @ 52 : 0d LdaUndefined
115 S> 0x1cc4f6c1ddc7 @ 53 : a9 Return
Constant pool (size = 5)
0x1cc4f6c1dcf1: [FixedArray] in OldSpace
- map: 0x1cc4a2f807b1 <Map>
- length: 5
0: 0x1cc4f6c1dca9 <SharedFunctionInfo find_me_inner>
1: 0x1cc403b900e9 <String[#7]: console>
2: 0x1cc403b8fbe9 <String[#3]: log>
3: 0x1cc4f6c1d921 <String[#1]: b>
4: 0x1cc4f6c1d909 <String[#1]: a>
Handler Table (size = 0)
[generated bytecode for function: find_me_inner]
Parameter count 1
Frame size 8
51 E> 0x1cc4f6c1de62 @ 0 : a5 StackCheck
68 S> 0x1cc4f6c1de63 @ 1 : 0c 05 LdaSmi [5]
70 E> 0x1cc4f6c1de65 @ 3 : 15 00 00 StaGlobal [0], [0]
0x1cc4f6c1de68 @ 6 : 26 fb Star r0
0x1cc4f6c1de6a @ 8 : 0d LdaUndefined
76 S> 0x1cc4f6c1de6b @ 9 : a9 Return
Constant pool (size = 1)
0x1cc4f6c1ddf1: [FixedArray] in OldSpace
- map: 0x1cc4a2f807b1 <Map>
- length: 1
0: 0x1cc4f6c1d921 <String[#1]: b>
Handler Table (size = 0)
5
a.js:6: ReferenceError: a is not defined
console.log(a)
^
ReferenceError: a is not defined
at find_me_test (a.js:6:15)
at a.js:9:1
其實重點就是 find_me_inner
,在裡面 a 會被當作 local variable 存進 r0 中,而 b 則是被存到 global 去了。
而我們在 console.log 的時候,a 與 b 都會跑到 global 去找,b 會輸出 5,而 a 沒有被定義,因此會拋出錯誤。
經典案例:hoisting
function find_me_test() {
var find_me_a = function() {
console.log(2)
}
function find_me_a() {
console.log(1)
}
find_me_a()
}
find_me_test()
請問輸出的結果會是 1 還是 2?
產生的 bytecode:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 8
0x2c1c9c39de1a @ 0 : 81 00 00 02 CreateClosure [0], [0], #2
0x2c1c9c39de1e @ 4 : 26 fb Star r0
21 E> 0x2c1c9c39de20 @ 6 : a5 StackCheck
44 S> 0x2c1c9c39de21 @ 7 : 81 01 01 02 CreateClosure [1], [1], #2
0x2c1c9c39de25 @ 11 : 26 fb Star r0
130 S> 0x2c1c9c39de27 @ 13 : 5c fb 02 CallUndefinedReceiver0 r0, [2]
0x2c1c9c39de2a @ 16 : 0d LdaUndefined
142 S> 0x2c1c9c39de2b @ 17 : a9 Return
Constant pool (size = 2)
0x2c1c9c39dda1: [FixedArray] in OldSpace
- map: 0x2c1cea3007b1 <Map>
- length: 2
0: 0x2c1c9c39dce1 <SharedFunctionInfo find_me_a>
1: 0x2c1c9c39dd39 <SharedFunctionInfo find_me_a>
Handler Table (size = 0)
[generated bytecode for function: find_me_a]
Parameter count 1
Frame size 24
52 E> 0x2c1c9c39df82 @ 0 : a5 StackCheck
61 S> 0x2c1c9c39df83 @ 1 : 13 00 00 LdaGlobal [0], [0]
0x2c1c9c39df86 @ 4 : 26 fa Star r1
69 E> 0x2c1c9c39df88 @ 6 : 28 fa 01 02 LdaNamedProperty r1, [1], [2]
0x2c1c9c39df8c @ 10 : 26 fb Star r0
0x2c1c9c39df8e @ 12 : 0c 02 LdaSmi [2]
0x2c1c9c39df90 @ 14 : 26 f9 Star r2
69 E> 0x2c1c9c39df92 @ 16 : 59 fb fa f9 04 CallProperty1 r0, r1, r2, [4]
0x2c1c9c39df97 @ 21 : 0d LdaUndefined
78 S> 0x2c1c9c39df98 @ 22 : a9 Return
Constant pool (size = 2)
0x2c1c9c39df09: [FixedArray] in OldSpace
- map: 0x2c1cea3007b1 <Map>
- length: 2
0: 0x2c1ca5e100e9 <String[#7]: console>
1: 0x2c1ca5e0fbe9 <String[#3]: log>
Handler Table (size = 0)
2
這是相當經典的一題,因為大多數人會以為 function find_me_a 的定義蓋過了前面的變數,所以答案是 1,不過正確答案是 2。這一題從 bytecode 乍看之下看不出來,因為 constant pool 中的兩個 function 同名,很難區別誰先誰後。
因此,我們來看看稍微改過一點的版本:
function find_me_test() {
var find_me_a = function() {
console.log(2)
}
function find_me_b() {
console.log(1)
}
function find_me_c() {
console.log(3)
}
find_me_a()
}
find_me_test()
bytecode:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 24
0x7625f39defa @ 0 : 81 00 00 02 CreateClosure [0], [0], #2
0x7625f39defe @ 4 : 26 fa Star r1
0x7625f39df00 @ 6 : 81 01 01 02 CreateClosure [1], [1], #2
0x7625f39df04 @ 10 : 26 f9 Star r2
21 E> 0x7625f39df06 @ 12 : a5 StackCheck
44 S> 0x7625f39df07 @ 13 : 81 02 02 02 CreateClosure [2], [2], #2
0x7625f39df0b @ 17 : 26 fb Star r0
178 S> 0x7625f39df0d @ 19 : 5c fb 03 CallUndefinedReceiver0 r0, [3]
0x7625f39df10 @ 22 : 0d LdaUndefined
190 S> 0x7625f39df11 @ 23 : a9 Return
Constant pool (size = 3)
0x7625f39de79: [FixedArray] in OldSpace
- map: 0x076283f007b1 <Map>
- length: 3
0: 0x07625f39dd61 <SharedFunctionInfo find_me_b>
1: 0x07625f39ddb9 <SharedFunctionInfo find_me_c>
2: 0x07625f39de11 <SharedFunctionInfo find_me_a>
Handler Table (size = 0)
[generated bytecode for function: find_me_a]
Parameter count 1
Frame size 24
52 E> 0x7625f39e082 @ 0 : a5 StackCheck
61 S> 0x7625f39e083 @ 1 : 13 00 00 LdaGlobal [0], [0]
0x7625f39e086 @ 4 : 26 fa Star r1
69 E> 0x7625f39e088 @ 6 : 28 fa 01 02 LdaNamedProperty r1, [1], [2]
0x7625f39e08c @ 10 : 26 fb Star r0
0x7625f39e08e @ 12 : 0c 02 LdaSmi [2]
0x7625f39e090 @ 14 : 26 f9 Star r2
69 E> 0x7625f39e092 @ 16 : 59 fb fa f9 04 CallProperty1 r0, r1, r2, [4]
0x7625f39e097 @ 21 : 0d LdaUndefined
78 S> 0x7625f39e098 @ 22 : a9 Return
Constant pool (size = 2)
0x7625f39e009: [FixedArray] in OldSpace
- map: 0x076283f007b1 <Map>
- length: 2
0: 0x0762037100e9 <String[#7]: console>
1: 0x07620370fbe9 <String[#3]: log>
Handler Table (size = 0)
2
可以看到在 StackCheck
之前,會依序建立 find_me_b
與 find_me_c
,最後才建立 find_me_a
。因此前面的案例也真相大白了,會把 function 宣告移到開頭去,先建立 function declaration 的 find_me_a
,才建立用 function expression 的 find_me_a
,因此答案是 2。
雖然說 hoisting 一直被認為是「概念上程式碼移動了」,不過在產生的 bytecode 裡面,順序還真的移動了,跟原本程式碼的順序不一樣。
經典案例:物件指向
最後來看一個十分經典的題目:
function find_me_test() {
var a = {n: 1}
a.x = a = {n: 2}
console.log(a.x)
}
find_me_test()
輸出的值會是多少呢?
從 bytecode 就可以很明顯看到執行順序:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 32
21 E> 0x18e26ba1dd4a @ 0 : a5 StackCheck
36 S> 0x18e26ba1dd4b @ 1 : 7d 00 00 29 CreateObjectLiteral [0], [0], #41
0x18e26ba1dd4f @ 5 : 26 fb Star r0
45 S> 0x18e26ba1dd51 @ 7 : 7d 01 01 29 CreateObjectLiteral [1], [1], #41
0x18e26ba1dd55 @ 11 : 27 fb fa Mov r0, r1
0x18e26ba1dd58 @ 14 : 26 fb Star r0
49 E> 0x18e26ba1dd5a @ 16 : 2d fa 02 02 StaNamedProperty r1, [2], [2]
64 S> 0x18e26ba1dd5e @ 20 : 13 03 04 LdaGlobal [3], [4]
0x18e26ba1dd61 @ 23 : 26 f9 Star r2
72 E> 0x18e26ba1dd63 @ 25 : 28 f9 04 06 LdaNamedProperty r2, [4], [6]
0x18e26ba1dd67 @ 29 : 26 fa Star r1
78 E> 0x18e26ba1dd69 @ 31 : 28 fb 02 08 LdaNamedProperty r0, [2], [8]
0x18e26ba1dd6d @ 35 : 26 f8 Star r3
72 E> 0x18e26ba1dd6f @ 37 : 59 fa f9 f8 0a CallProperty1 r1, r2, r3, [10]
0x18e26ba1dd74 @ 42 : 0d LdaUndefined
81 S> 0x18e26ba1dd75 @ 43 : a9 Return
Constant pool (size = 5)
0x18e26ba1dcb1: [FixedArray] in OldSpace
- map: 0x18e291a807b1 <Map>
- length: 5
0: 0x18e26ba1dc51 <ObjectBoilerplateDescription[3]>
1: 0x18e26ba1dc79 <ObjectBoilerplateDescription[3]>
2: 0x18e26ba1d919 <String[#1]: x>
3: 0x18e288f100e9 <String[#7]: console>
4: 0x18e288f0fbe9 <String[#3]: log>
Handler Table (size = 0)
undefined
我來簡化一下 bytecode:
CreateObjectLiteral [0], [0], #41 // acc = {n: 1}
Star r0 // r0 = acc
CreateObjectLiteral [1], [1], #41 // acc = {n: 2}
Mov r0, r1 // r1 = r0
Star r0 // r0 = acc (所以 r0 會是 {n:2},r1 是 {n:1})
StaNamedProperty r1, [2], [2] // r1.x = acc(r1.x = {n: 2})
LdaNamedProperty r0, [2], [8] // acc = r0.x
Star r3 // r3 = acc
console.log(r3)
可以看到原本一開始的 a 是 r0,可是之後被放到 r1 去,而最後印出來的值是 r0 的 x 而不是 r1,所以 a 已經被改變了。
若是把 bytecode 再轉回 JavaScript,會長的像這樣:
/*
var a = {n: 1}
a.x = a = {n: 2}
console.log(a.x)
*/
var a = {n: 1}
var old_a = a
a = {n: 2}
old_a.x = a
console.log(a.x)
這一題的關鍵就在於 a.x = a = {n: 2}
前面的那個 a.x
會是舊的那個 a 而不是新的。
結語
從這一篇當中我們看到了幾個經典案例的 bytecode,有時候藉由分析 bytecode,可以更理解在 V8 眼中這段程式碼是什麼樣子。