Day06:從經典案例看 bytecode


前言

在這一篇裡面,我們會來看一些比較經典的例子,藉由分析 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_bfind_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 眼中這段程式碼是什麼樣子。

#javascript
V8 在處理 JavaScript 的程式碼時,會先將程式碼轉換成中間碼(bytecode)才執行。因此,藉由觀察 V8 bytecode,可以更理解一段 JavaScript 程式碼在 V8 眼裡是什麼樣子。這個系列文會以一系列的簡單程式碼為例,帶大家一起研究 bytecode






Related Posts

r3:0 異世界網站挑戰 Lv. 11 ~ 15 攻略

r3:0 異世界網站挑戰 Lv. 11 ~ 15 攻略

Airflow 動手玩系列文介紹

Airflow 動手玩系列文介紹

CSS 魔術師 Houdini API 介紹

CSS 魔術師 Houdini API 介紹



Comments











Sponsored