Hoisting, 그리고 함정 (var, let, const)
Hoisting
예제를 먼저 살펴보자.
console.log(a); // undefined
var a;
변수를 참조하는 코드가 변수 선언문보다 앞에 있다.
인터프리터에 의해 한 줄씩 순차적으로 실행되는 자바스크립트의 특징상,
변수를 참조하는 코드인 console.log(a)
가 먼저 실행되고
참조할 변수 a가 선언되지 않았으므로 참조 에러 (ReferenceError)가 발생할 것 같지만,
undefined가 출력된다.
그 이유는, 변수 선언문이 Hoisting
되기 때문이다.
자세히 알아보자.
변수 선언은 코드가 순차적으로 실행되며 실행될 것으로 보이지만, 실제로는 그렇지 않다.
순차적으로 실행되는 시점, 즉 런타임 (Runtime)이 아니라 그 이전에 실행된다.
자바스크립트는 코드를 순차적으로 실행하기 전에 소스코드의 평가 과정을 거친다.
이 평가 과정에서 변수 선언문, 함수 선언문 등 모든 선언문을 코드 내에서 전부 찾아내
스코프에 등록하고, 이 과정이 끝나면 모든 선언문을 제외한 코드들을 순차적으로 실행하고,
코드를 실행하며 필요한 선언문들을 스코프에서 꺼내서 사용한다.
정리해 보자면, 자바스크립트 코드는 어떤 선언문이든, 코드 내의 어디에 있든 상관 없이
먼저 실행이 되고, 그로 인해 선언문이 어디에 있는지와는 상관 없이 어디서든 변수를 참조할 수 있다.
이렇듯, 변수 선언문이 가장 먼저 실행되기 때문에 이 선언문들이 최상단에 끌어올려져 동작하는
것처럼 보이는데, 이것이 자바스크립트 고유의 특징인 호이스팅 (Hoisting)
이다.
(주의)
console.log(a); // undefined
var a = 10;
위 예제와 같이 값이 할당이 된다 해도,
호이스팅 되면서 변수의 값이 undefined로 초기화
되기 때문에
여전히 undefined를 출력한다.
(선언문만 호이스팅 되고, 할당은 호이스팅 되지 않는다 라고 이해해도 좋다. 아래 let 키워드
설명에서 좀 더 자세히 알아보자)
var, let, const?
var, let, const 키워드는 전부 호이스팅 된다.
하지만, var를 제외한 두 키워드는 호이스팅이 되지 않는 것처럼 동작한다.
이것이 바로 함정이다.
그럼, 변수를 선언하는 var, let, const 키워드들의 특징을 알아보자.
var
- 중복 선언 가능 var 키워드로 선언한 변수는 중복 선언이 가능하다.
var a = 1;
var b = 1;
var a = 100;
// 같은 스코프 내에서 var 키워드의 중복 선언이 가능하다.
var b;
// 초기화문이 없는 선언문은 무시된다.
console.log(a); // 100
console.log(b); // 1
위와 같이 초기화문 (선언과 동시에 초기값을 할당하는 문)이 있는 선언문은 var 키워드가 없는
것처럼 동작하고, 초기화문이 없는 선언문은 무시된다.
만약 위와 같이 동일한 이름으로 변수가 선언되어 있는 것을 모르고 변수를 중복 선언할 시에
의도치 않은, 생각지 못한 오류를 일으킨다.
- 함수 레벨 스코프 var 키워드로 선언한 변수는 오로지 함수의 코드 블록만을 지역 스코프로 인정하기 때문에 함수 외부에서 var 키워드로 선언한 변수는 코드 블록 내에서 선언해도 모두 전역 변수가 된다.
var a = 1;
if (true) {
var a = 100;
// 전역 변수인 a가 중복 선언
// 중복 선언으로 인한 예기치 못한 오류를 일으킨다.
}
console.log(a); // 100
for 문에서 var 키워드로 선언한 변수 역시 전역 변수가 된다.
var i = 10;
for (var i = 0; i < 5; i++) {
console.log(i); // 0 1 2 3 4
}
console.log(i); // 5
// 전역 변수 i가 의도치 않게 변경되는 오류를 일으켰다.
- Hoisting var 키워드로 선언한 변수는 호이스팅에 의해 스코프의 상단으로 끌어 올려진 것처럼 동작한다.
// 호이스팅으로 이미 변수 a가 선언되었다.
// 변수 a는 undefined로 초기화.
console.log(a); // undefined
// 변수 a에 값 10을 할당
a = 10;
console.log(a); // 10
// 가장 아래에 선언문이 위치하지만, 호이스팅 된다.
var a;
let
- 중복 선언 불가 var 키워드와는 다르게 변수를 중복 선언하면 Syntax Error이 발생한다.
let a = 10;
let a = 20; // SyntaxError: Identifier 'a' has already been declared
- 블록 레벨 스코프 모든 코드 브록 (함수, if문, for문, while문, try/catch문 등)을 지역스코프로 인정하는 블록 레벨 스코프를 따른다.
let a = 1; // 전역 변수
{
let a = 2; // 지역 변수
let b = 3; // 지역 변수
}
console.log(a); // 1
console.log(b); // ReferenceError: b is not defined
위와 같이, 전역 스코프에서 선언된 변수 a와 코드 블록 내에서 선언된 지역 변수 a는
별개의 변수이다. 때문에 전역 스코프에서 변수 a를 참조하게 되면,
전역 변수 a를 참조하고, 코드 블록 내의 지역 변수 a는 참조하지 않는다.
마찬가지로, 전역에서 변수 b를 참조하게 된다면 ReferenceError을 출력한다.
- Hoisting var 키워드와는 다르게, let 키워드로 선언된 변수는 호이스팅이 일어나지 않는 것처럼 작동한다.
console.log(a); // ReferenceError: a is not defined
let a;
var 키워드에서는 undefined를 출력했는데, let 키워드는 ReferenceError을 출력한다.
위에서 간략하게 설명했듯이,
var 키워드로 선언한 변수는 코드 블록들이 순차적으로 실행되기 전에, 자바스크립트 엔진에 의해
암묵적으로 “선언 단계”와 “초기화 단계”가 동시에 진행된다.
선언 단계에서 스코프에 변수를 등록하고, 초기화 단계에서 변수를 undefined로 초기화 하는 것이다.
이 때문에 var 키워드에서는 undefined를 출력하는 것이다.
하지만
, let 키워드에서는 “선언 단계”와 “초기화 단계”가 분리되어서 진행된다.
코드 블록들이 순차적으로 실행되기 전에 선언 단계가 먼저 실행되지만, 초기화 단계는 변수 선언문에
도달했을 때 실행된다.
초기화 단계 전에 변수에 접근하려고 하면 참조 에러 ReferenceError을 출력하기 때문에
위의 예제와 같은 결과가 나온 것이다.
이렇게, let 키워드로 선언한 변수는 선언 단계에서 초기화 단계 시작 지점까지 변수를
참조할 수 없는데, 이 구간을 일시적 사각지대 (TDZ: Temporal Dead Zone)
라고 한다.
위의 ‘변수의 생명 주기’를 나타낸 그림을 참고하자.
선언 단계에서 초기화 단계 직전에 변수를 참조하려고 하면, ReferenceError를 출력하고,
그 사이가 바로 일시적 사각지대, TDZ이다.
초기화 단계에서는 선언된 변수를 undefined로 초기화 하고,
할당 단계에서는 선언된 변수에 값을 할당한다.
이렇게 보면, let은 호이스팅이 되지 않는 것처럼 보인다.
하지만, 호이스팅이 되지 않는 것이 아니다!!
let a = 1; // 전역 변수
{
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 2; // 지역 변수
}
위의 예제를 보면, a를 참조하는 코드가 ReferenceError을 출력하고 있다.
만약에 호이스팅이 발생하지 않았다면, 지역 변수인 a를 참조하려고 하는게 아닌,
전역 변수 a를 참조하기 때문에 1을 출력해야 하는데,
호이스팅은 발생했지만 초기화가 되지 않았기 때문에 ReferenceError: initialization을 출력한다.
이것이 바로, let 키워드는 호이스팅은 되지만, 호이스팅 되지 않는 것처럼 동작한다는 것
이다.
Const
const 키워드는 let 키워드와 특징이 대부분 동일하다.
때문에, let 키워드가 다른 부분을 중심으로 알아보자.
- 선언과 초기화가 동시에 이루어져야 한다. let 키워드에서는 선언만 해도 문제가 없었지만, const 키워드에서는 초기화 없이 선언만 하게 되면 initializer Error가 발생한다.
let a;
console.log(a); // undefined
const a; // SyntaxError: Missing initializer in const declaration
- 재할당 불가 var, let 키워드로 선언한 변수는 재할당이 가능하지만, const 키워드로 선언한 변수는 재할당이 불가능하다.
var a = 1;
a = 2;
let b = 10;
b = 20;
const c = 100;
c = 200; // TypeError: Assignment to constant variable.
만약 const 키워드로 선언한 변수를 재할당하려고 시도한다면
TypeError: Assignment to constant variable.
에러가 출력된다.
- 상수 const 키워드로 선언한 변수는 재할당이 불가능하기 때문에, 변하지 않는 상수를 선언할 때 사용된다. 변수는 변하는 값인데, 그 변수를 선언하는 const가 상수를 선언한다? 라는 의문을 갖을 수 있지만, const 키워드 역시 값을 저장하기 위한 메모리 공간이 필요하므로 변수라고 부른다.
- 참조 자료형의 값을 변경한다
원시 자료형의 경우에는 재할당 없이는 값을 변경할 수 있는 방법이 없다.
때문에, 재할당이 금지된 const 키워드에서 원시 자료형은 값을 변경할 수 없는데,
재할당 없이
직접 변경
으로 값을 변경할 수 있는 참조 자료형의 값은 변경할 수 있다.
const obj = {
a: 1,
b: 5,
};
obj.a = 5;
console.log(obj); // {a: 5, b: 5}
const arr = [1, 3, 5];
arr[0] = 0;
console.log(arr); // [0, 3, 5]
결국, const 키워드는 재할당을 금지 할 뿐
이고, 불변
한 것은 아니다.
정리해보자.
- 모든 선언문 (var, let, const, function, class 등) 은 호이스팅 된다.
단, let, const, class는 호이스팅 되지 않는 것처럼 동작한다.
- 이제 var는 사용하지 않는다. 재할당이 필요한 경우 let을 사용하고, 기본적으로 const를 사용한다.