해당 글은 node v16.13.0을 기준으로 작성되었습니다.
본인이 사용하는 node 버전에 따라 인터프리터가 생성하는 바이트코드가 달라질 수도 있습니다.
일반적으로는 var는 호이스팅이 되어서 중복 선언이 가능하다고 알려져있고,
let, const는 호이스팅이 되지 않아 중복 선언이 되지 않는다고들 알고 있다.
여기서 더 나아가서 let, const가 호이스팅이 되지만 TDZ에 의해서 변수가 초기화되기 전까지는 접근이 불가능하다는걸 알고 있는 사람들도 많을 것이다.
그러면 여기서 더 더 나아가서, v8 엔진이 자바스크립트 코드를 어떻게 해석하길래 호이스팅이나 TDZ가 우리가 생각하는대로 동작하는지 알아보자.
바이트코드를 출력하기 위해서는 --print_bytecode 옵션을 이용하면 된다.
ex) node --print_bytecode test.js > test.log
예제 1 - 변수 할당 후 초기화 하는 경우
예제1에서는 var, let, const가 각각 선언과 초기화가 어떻게 이루어지는지 확인해보자.
var
function testFunc() {
var test;
test = 2;
}
testFunc();
24 S> 0x25afcaa38376 @ 0 : 0d 02 LdaSmi [2]
0x25afcaa38378 @ 2 : c3 Star0
47 S> 0x25afcaa38379 @ 3 : 0d 03 LdaSmi [3]
0x25afcaa3837b @ 5 : c3 Star0
오른쪽에 어셈블리어 비슷한 것이 보인다. 해석해보자.
1) LdaSmi [2] = Load accumulator Small integer [2]
누산기에 small integer 타입으로 2를 로드한다.
2) Star0 = Store accumulator r0
누산기의 값(=2)을 0번째 레지스터에 저장한다.
1) LdaSmi [3] = Load accumulator Small integer [3]
누산기에 small integer 타입으로 3를 로드한다.
2) Star0 = Store accumulator r0
누산기의 값(=3)을 0번째 레지스터에 저장한다.
test 변수를 이미 r0에 할당하여 사용하는 것으로 보인다.
즉, test 변수는 호이스팅이 된 상태라는 것을 알 수 있다.
let
function testFunc() {
test = 2;
let test = 3;
}
testFunc();
0x840bd43838e @ 0 : 10 LdaTheHole
0x840bd43838f @ 1 : c3 Star0
24 S> 0x840bd438390 @ 2 : 0d 02 LdaSmi [2]
0x840bd438392 @ 4 : c2 Star1
0x840bd438393 @ 5 : 0b fa Ldar r0
29 E> 0x840bd438395 @ 7 : a9 00 ThrowReferenceErrorIfHole [0]
0x840bd438397 @ 9 : 19 f9 fa Mov r1, r0
47 S> 0x840bd43839a @ 12 : 0d 03 LdaSmi [3]
0x840bd43839c @ 14 : c3 Star0
7줄부터는 let test = 3;을 구현한 내용이므로 6줄까지만 살펴보자.
LdaTheHole
Star0
r0에 Hole을 저장한다.
LdaSmi [2]
Star1
r1에 2를 저장한다.
Ldar r0
ThrowReferenceErrorIfHole [0]
누산기에 r0(=Hole)을 로드하고, 그 값이 Hole일 경우 ReferenceError를 Throw한다.
여기서 TDZ가 Hole이라는 예약된 값을 이용하여 구현되고 있다는 것을 알 수 있다.
의사 코드로 작성하면 다음과 같다.
acc = Hole
r0 = acc
acc = 2
r1 = acc
acc = r0
if (acc == Hole) throw ReferenceError
r0 = r1
acc = 3
r0 = acc
const
function testFunc() {
test = 2;
const test = 3;
}
testFunc();
0x33435e4f838e @ 0 : 10 LdaTheHole
0x33435e4f838f @ 1 : c3 Star0
24 S> 0x33435e4f8390 @ 2 : 0d 02 LdaSmi [2]
0x33435e4f8392 @ 4 : c2 Star1
0x33435e4f8393 @ 5 : 0b fa Ldar r0
29 E> 0x33435e4f8395 @ 7 : a9 00 ThrowReferenceErrorIfHole [0]
0x33435e4f8397 @ 9 : 64 61 01 fa 00 CallRuntime [ThrowConstAssignError], r0-r0
49 S> 0x33435e4f839c @ 14 : 0d 03 LdaSmi [3]
0x33435e4f839e @ 16 : c3 Star0
전체적으로 let과 비슷하게 작성된 것을 볼 수 있다.
7줄을 보면 ThrowConstAssignError가 추가된 것을 볼 수 있는데, const를 할당하지 않을 경우 에러를 발생시키기 위한 코드로 보인다.
예제2 - 변수를 초기화하기 전에 사용할 경우
변수를 할당하기 전에 사용하는 경우도 확인해보자.
var
function testFunc() {
console.log(test)
var test = 2;
}
testFunc();
24 S> 0x12e45ee78396 @ 0 : 21 00 00 LdaGlobal [0], [0]
0x12e45ee78399 @ 3 : c1 Star2
32 E> 0x12e45ee7839a @ 4 : 2d f8 01 02 LdaNamedProperty r2, [1], [2]
0x12e45ee7839e @ 8 : c2 Star1
32 E> 0x12e45ee7839f @ 9 : 5d f9 f8 fa 04 CallProperty1 r1, r2, r0, [4]
55 S> 0x12e45ee783a4 @ 14 : 0d 02 LdaSmi [2]
0x12e45ee783a6 @ 16 : c3 Star0
이번에는 작성한 코드와 비교하면서 분석해보자.
1) console.log(test)
LdaGlobal [0], [0]
Star2
LdaNamedProperty r2, [1], [2]
Star1
CallProperty1 r1, r2, r0, [4]
global에 등록되어 있는 constant pool의 첫 번째 인덱스를(=console) r2에 저장하고, r2의 1 번째 인덱스의 값(=log)을 r1에 저장한다.
즉, console.log(r0)으로 해석할 수 있다. (r0에 값을 할당한 적이 없으므로 r0=undefined)
이 구문을 해석하기 위해 필요한 바이트코드를 첨부한다.
Constant pool (size = 2)
0x12e45ee78341: [FixedArray] in OldSpace
- map: 0x017721b012c1 <Map>
- length: 2
0: 0x22f989f390b1 <String[7]: #console>
1: 0x22f989f0c0b1 <String[3]: #log>
2) var test = 2;
LdaSmi [2]
Star0
누산기에 2를 로드하고 r0에 적재한다. 즉, r0을 console.log로 출력하고 나서 2로 초기화하고 있다.
console.log에서 r0을 출력하고 있고, test 변수도 r0을 참조하는 것으로 보이므로 호이스팅 되고 있다는 것을 알 수 있다.
let, const
변수 할당과 초기화를 동시에 할 경우 let과 const는 bytecode가 완전히 동일하므로 const 예제는 let으로 대신한다.
function testFunc() {
console.log(test)
let test = 2;
}
testFunc();
0x39597af3839e @ 0 : 10 LdaTheHole
0x39597af3839f @ 1 : c3 Star0
24 S> 0x39597af383a0 @ 2 : 21 00 00 LdaGlobal [0], [0]
0x39597af383a3 @ 5 : c1 Star2
32 E> 0x39597af383a4 @ 6 : 2d f8 01 02 LdaNamedProperty r2, [1], [2]
0x39597af383a8 @ 10 : c2 Star1
0x39597af383a9 @ 11 : 0b fa Ldar r0
36 E> 0x39597af383ab @ 13 : a9 02 ThrowReferenceErrorIfHole [2]
32 E> 0x39597af383ad @ 15 : 5d f9 f8 fa 04 CallProperty1 r1, r2, r0, [4]
55 S> 0x39597af383b2 @ 20 : 0d 02 LdaSmi [2]
0x39597af383b4 @ 22 : c3 Star0
바이트코드를 쪼개서 분석해보자.
0x39597af3839e @ 0 : 10 LdaTheHole
0x39597af3839f @ 1 : c3 Star0
var와는 다르게 Hole이라는 값을 r0에 저장하고 있다.
24 S> 0x39597af383a0 @ 2 : 21 00 00 LdaGlobal [0], [0]
0x39597af383a3 @ 5 : c1 Star2
32 E> 0x39597af383a4 @ 6 : 2d f8 01 02 LdaNamedProperty r2, [1], [2]
0x39597af383a8 @ 10 : c2 Star1
0x39597af383a9 @ 11 : 0b fa Ldar r0
36 E> 0x39597af383ab @ 13 : a9 02 ThrowReferenceErrorIfHole [2]
32 E> 0x39597af383ad @ 15 : 5d f9 f8 fa 04 CallProperty1 r1, r2, r0, [4]
1~4줄과 7줄은 var와 같다. console.log를 찍기 위해 세팅하는 과정이다.
5~6줄을 살펴보자.
0x39597af383a9 @ 11 : 0b fa Ldar r0
36 E> 0x39597af383ab @ 13 : a9 02 ThrowReferenceErrorIfHole [2]
레지스터 r0을 누산기에 로드한 후 해당 값이 Hole이면 ReferenceError를 내뱉는다.
요약
알 수 있는 점은 다음과 같다.
- var, let, const 전부 호이스팅 된다.
- var는 초기화되지 않은 변수에 대해 에러처리를 하지 않는다.
- let, const는 변수를 Hole이라는 값으로 초기화해주어 이 값을 이용하여 에러처리를 한다.
- const는 할당과 초기화가 동시에 이루어지는지 체크하는 코드가 추가된다.
바이트 코드를 뜯어봤지만 이 역시 추상화 되어있는 코드이기 때문에 v8 엔진이 어떻게 돌아가는지 자세히는 알 수 없다.
하지만 javascript 코드를 분석할 때 보다는 좀 더 이해가 깊어진 것 같다.
가끔씩 이렇게 바이트코드를 분석해 보는것도 좋을 듯 하다.
Reference
https://github.com/v8/v8/tree/main/src/interpreter
https://stackoverflow.com/questions/66472730/what-is-ldakeyedproperty-in-v8-byte-code
https://github.com/danbev/learning-v8
https://frogred8.github.io/docs/005_compiled_js_file/
'BackEnd > Node.js' 카테고리의 다른 글
[Nodejs] 비동기 API라고 해서 다 같은 비동기가 아니다. (0) | 2024.04.27 |
---|