var와 let의 차이를 공부하다 나온 호이스팅과 TDZ에 대해 알아보자.
호이스팅과 TDZ
호이스팅과 TDZ로 인해 기존에 다른 프로그래밍 언어를 배운 사람들은 Js코드가 이상하다고 느낀다. 나도 그랬다. 하지만 Js를 쓰다보면 호이스팅덕분에 더 편한것 같다.
호이스팅 (hoisting)
Js는 함수의 코드를 실행하기 전에 함수 선언에 대한 메모리부터 할당한다. 덕분에 스크립트내에서 함수 선언의 위치는 중요하지 않다. 스크립트내에서 함수 호출을 함수 선언보다 먼저 해도 문제가 없다. 또 호이스팅은 스코프 단위로 일어난다. 예를 보며 이해해보자.
callFunc(); // 함수 호출
function callFanc() {
console.log("나 함수!");
} // 함수 선언
가장 처음 배운 언어가 C언어인 내 입장에서는 살짝 당황 스럽다. 하지만 Js에서는 선언하지도 않은 함수를 호출하는 것이 가능하다. 앞서 말했듯 Js의 인터프리터는 변수와 함수의 메모리를 선언전에 미리 할당한다. 이를 호이스팅(hoisting)이라고 한다. 호이스팅은 선언만 해당된다. 초기화, 할당은 해당되지 않는다. 하지만 var는 호이스팅 시 undefined로 초기화까지 함께한다. let과 const는 선언과 초기화가 별도로 이루어져 선언 전에 호출되어 호이스팅이 되어도 초기화가 안되어 있기 때문에 오류가 발생하게 된다. 이러한 호이스팅 덕분에 Js에서는 함수를 아무데나 선언 할 수 있다. 그래도 C언어로 프로그래밍을 처음 접한 나는 함수를 위에 선언하는 습관이 있다. Js를 하며 코드를 선언, 호출 별로 그룹화 하던 습관을 용도 별로 그룹화 하는 습관을 길러야 겠다.
TDZ( Temporal Dead Zone )
1. TDZ란?
이름에서 알 수 있듯 TDZ는 Zone 즉, 지역을 의미한다. TDZ는 구문(let, const, class 등)의 호이스팅으로 선언만 되고 초기화 되지 않은 지역을 의미한다.
앞서 호이스팅에서 설명 했듯 호이스팅은 블록 단위로 발생한다. 즉 블록이 생성될때 블록안의 호이스팅으로 선언을 먼저 하게되는데 선언 후 초기화되기 전까지 호이스팅 된 구문을 TDZ에 있다고 말한다. TDZ에 있는 구문를 호출하면 초기화 되지 않았기 때문에 레퍼런스 오류가 생긴다. 하지만 var는 선언과 동시에 초기화가 이루어지기 때문에 var의 TDZ가 없다.
{ // TDZ starts at beginning of scope
console.log(bar); // undefined
console.log(foo); // ReferenceError
var bar = 1;
let foo = 2; // End of TDZ (for foo)
}
위 코드는 MDN의 예시이다. var는 호이스팅과 동시에 초기화가 되어 TDZ가 없어 에러가 발생하지 않지만 foo는 호이스팅되어 선언은 되어있지만 초기화되지 않은 TDZ에서 호출했다. 따라서 레퍼런스 에러가 발생한다.
let a = 10;
if (true){
console.log(a);
let a = 10;
}
위 예시 코드는 호이스팅이 블록단위로 발생한다는 것을 증명하는 코드이다. 만약 블록단위가 아니라면 console.log(a)의 결과는 10이 나와야 한다. 하지만 그 아래있는 let a = 10이 호이스팅 되어 console.log(a)는 TDZ에 있게된다. 따라서 레퍼런스 오류가 발생한다.
2. TDZ를 갖는 구문
(1) const
// Does not work!
pi; // throws `ReferenceError`
const pi = 3.14;
// Works!
const pi = 3.14;
pi; // => 3.14
(2) let
// Does not work!
count; // throws `ReferenceError`
let count;
count = 10;
// Works!
let count;
count; // => undefined
count = 10;
count; // => 10
여기서 살짝 내가 잘못 이해했나 하고 헷갈렸다. 나는 호이스팅을 선언을 당겨오는 것이라 생각했다. 또, TDZ는 선언부터 초기화 사이의 지역이라고 이해했고 앞의 글에도 그렇게 적었다. 근데 왜 위 Works! 코드에서 선언을 하고 10으로 초기화하기 전에 호출했을 때 count는 undefined를 반환하는가? 잠깐 생각해보면 알 수 있다. 사실 let count; 에서 undefined로 초기화까지 된것이다. 초기화가 안됐다면 레퍼런스오류가 발생했을 것이다. 여기서 우리는 호이스팅이 단순 let count; 코드를 맨 위로 올리는것이 아님을 알 수 있다. let count;는 선언과 초기화가 동시에 일어난는 것이고 호이스팅은 그중 선언만을 먼저 하는 것이기에 TDZ가 생기는 것이다.
(3) class
// Does not work!
const myNissan = new Car('red'); // throws `ReferenceError`
class Car {
constructor(color) {
this.color = color;
}
}
// Works!
class Car {
constructor(color) {
this.color = color;
}
}
const myNissan = new Car('red');
myNissan.color; // => 'red'
(4) constructor() 내부의 super()
// Does not work!
class MuscleCar extends Car {
constructor(color, power) {
this.power = power;
super(color);
}
}
const myCar = new MuscleCar(‘blue’, ‘300HP’); // `ReferenceError`
// Works!
class MuscleCar extends Car {
constructor(color, power) {
super(color);
this.power = power;
}
}
const myCar = new MuscleCar('blue', '300HP');
myCar.power; // => '300HP'
아직 프로그래밍과 Js를 시작하는 단계라 코드가 잘 이해가 가진 않는다. 이 부분은 일단 예시를 적어 두고 나중에 클래스, 상속을 더 공부하고 설명을 추가 하겠다. (2021.10.05)
(5) 기본 함수의 매개변수
// Does not work!
const a = 2;
function square(a = a) {
return a * a;
}
square(); // throws `ReferenceError`
(a = a) 이부분은 자기가 자기 자신을 참조하는 행위가 된다. 전역변수 a보다 같은 스코프안에 있는 지역변수 a를 먼저 참조하게 되는데 a는 아직 초기화가 안됐는데 a를 참조하기때문에 레퍼런스 에러가 생긴다. 함수안의 a는 TDZ에 있다.
// Works!
const init = 2;
function square(a = init) {
return a * a;
}
square(); // => 4
전역변수의 이름을 바꿔주면 a = init에서 같은 스코프안에 init이 없기때문에 외부 에서 init을 찾게 된다. init을 찾은 후 a를 init으로 초기화 해주기때문에 에러가 발생하지 않는다.
3. var, function, import
계속 강조 했듯 var는 TDZ가 없다. 호이스팅 시 선언과 초기화가 같이 되기 때문에 undefined로 초기화된다. function의 선언과 import도 호이스팅 되기때문에 함수 선언을 어디에서 해도 호출할때 문제가 없다.
// Works, but don't do this!
value; // => undefined
var value;
// Works!
greet('World'); // => 'Hello, World!'
function greet(who) {
return `Hello, ${who}!`;
}
// Works!
greet('Earth'); // => 'Hello, Earth!'
// Works!
myFunction();
import { myFunction } from './myModule';
4. TDZ에서 typeof 연산자의 동작
TDZ에서 typeof 연산자를 사용하게 되면 레퍼런스 에러가 발생한다. 선언되지 않은 변수에 typeof 연산자를 쓰면 결과는 undefined이다.
typeof notDefined; // => 'undefined'
typeof variable; // throws `ReferenceError`
let variable;
function doSomething(someVal) {
// Function scope
typeof variable; // => undefined
if (someVal) {
// Inner block scope
typeof variable; // throws `ReferenceError`
let variable;
}
}
doSomething(true);
이를 이용해 스코프내부에 해당 변수가 선언 됐는지를 확인 할 수 있다. 선언 된 변수에 typeof연산자를 쓰면 레퍼런스 에러가 발생하고 선언되지 않았다면 결과는 undefined일것이다.
참조
Hoisting: https://developer.mozilla.org/ko/docs/Glossary/Hoisting
TDZ: https://ui.toast.com/weekly-pick/ko_20191014
'Languages > JS∕TS' 카테고리의 다른 글
[TS] How to get type from property of objects in array (0) | 2022.07.02 |
---|---|
[TS] interface, impliments, extends 예시 (0) | 2022.02.23 |
[TS] 기본 자료형 (0) | 2022.02.22 |
[코어 자바스크립트] 메모리 동작과 mutability, immutability (0) | 2021.12.25 |
[JS] 1. 기본 문법과 변수, 상수 (0) | 2021.10.05 |