개요
자바스크립트의 단골 면접중에서 유명한 문제가 있다.
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
}
위와 같은 코드가 주어지고 출력되는 결과를 에측해보라는 문제이다. 보통 클로저의 개념에 대해서 물어볼때 가장 흔하게 물어보는 코드이다. 하지만 주의해야할 점이 있다. 해당 코드의 for
문에서 선언 부분을 var
를 사용하지 않고 ES6에서 주로 사용하는 let
을 사용하게 되면 결과가 완전히 달라지게 된다. var
를 사용한 결과는 5 5 5 5 5
이고 let
을 사용하게 되면 0 1 2 3 4
가 출력된다. var
를 사용한 출력된 결과는 친숙했지만 let
을 사용해서 나온 결과를 공부하면서 정리한 내용을 바탕으로 해당 포스트를 작성하게 되었다.
var
ES6 이전의 자바스크립트에서는 var
키워드를 사용해서 변수를 정의하게 된다. 하지만 해당 키워드(ES6에서도 var를 사용할 수 있다)의 가장 큰 특징은 함수 레벨 스코프만을 지원한다는 점이다. 즉 해당 함수 블록 내에서 선언된 변수는 함수 블록 내에서만 유효하고, 특이하게도 함수내에서 중첩된 다른 블록들에서 사용된 변수들도 해당 함수 내에서는 사용할 수 있다.
(function fn() {
var a = 10;
console.log(b);
if (true) {
var b = 20;
}
console.log(a);
console.log(b);
})()
> undefined
> 10
> 20
위 코드를 보면 뭔가 이상한 점을 찾을 수 있다. b
라는 변수는 선언되기도 전에 사용했는데 오류를 발생하지 않는다. 어떻게 이런일이 가능할걸까? 이는 var
변수의 라이프사이클을 제대로 이해해야 한다. var
를 사용해서 변수를 선언한 함수 블록내로 들어가게 되면 메모리 공간이 해당 변수에게 할당되면서 undefined
값을 가지게 된다. 그리고 해당 변수를 선언한 식(expression)에 다다르게 되면 그때서야 초기값이 설정된다. 만약 초기값을 설정하지 않았다면 그대로 undefined
값을 가진다. 이해가 잘 되지 않는다면 아래의 코드를 참고해보자.
var a = 30;
(() => {
console.log(a);
a = 15;
console.log(a);
var a = 10;
console.log(a);
})();
console.log(a);
위 코드의 실행 결과는 30 undefined 15 10 Uncaught ReferenceError: a is not defined
이다. 맨 처음 함수가 실행될때 a
라는 변수에 메모리가 할당되면서 undefined
라는 값으로 초기화된다. 그리고 a = 15
가 수행되고 a
는 15라는 값을 가진다. 그리고 제일 마지막으로 var a = 10
를 최종적으로는 a
는 10이라는 값이 할당된다. 마지막으로 IIFE
의 스코프가 끝나고 글로벌 스코프에서는 a
라는 변수에는 맨 처음에 할당한 30이란 값이 호출된다.
const let
ES6에서 var
를 대체하기 위해서 추가된 변수 선언 방법이다. let
은 var
와 거의 비슷하게 동작하지만 변수의 라이프사이클이 완전히 다르다. let
과 const
로 선언된 변수는 블록 레벨 스코프를 가지게 된다. 변수가 선언된 블록 내로 들어오가면 변수에 메모리가 할당되지만 해당 변수는 초기화되지 않는다. 이때 변수가 선언된 곳보다 먼저 변수에 접근하거나 값을 바꾸려고 하면 ReferenceError
가 발생한다. 이렇게 선언된 변수들을 이와 같이 선언된 곳보다 먼저 접근하거나 할당하려고 하면 에러가 발생하는데 이를 TDZ
라고 합니다. 그리고 변수가 선언된 곳에서 해당 변수를 값을 할당시키게 되며 값을 할당하지 않는다면 해당 변수는 undefined
값을 가진다.
const
는 let
과 거의 비슷하게 동작한다. 하지만 const
를 사용해 선언된 변수는 재할당이 불가능하며 선언과 동시에 값을 할당해 주어야 한다. 그리고 할당된 값을 immutable
하도록 만들어 주진 않는다. 잘 이해가 안된다면 아래 예제를 살펴보자.
const obj = {};
obj.a = 10;
obj.b = 20;
console.log(obj); > {a: 10, b: 20}
const obj2 = {};
obj2 = obj > Uncaught TypeError: Assignment to constant variable.
만약 obj를 immutable
하게 만들고 싶다면 Object.freeze()
을 이용한다.
정리하자면 아래 표와 같다.
var
는 함수내에서 호이스팅이 일어나며 함수 단위의 스코프를 가진다. 또한 특이하게도 global property를 생성한다.let
과const
는 블록 단위의 스코프를 가지며TDZ
로 들어가게 된다. 또한 글로벌 프로퍼티를 생성하지 않는다.
var and let in loop
var
와 let
에 대한 설명을 쭉 서술했지만 아직 처음 질문은 제대로 설명되지 않는다. 개요에 있는 코드를 정확히 이해하려면 바벨을 사용해서 코드를 트렌스파일 해보면 이해가 쉽다.
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
}
위 코드를 변환하게 되면 아래와 같다.
var _loop = function _loop(i) {
setTimeout(function () {
console.log(i);
}, 1000 * i);
};
for (var i = 0; i < 5; i++) {
_loop(i);
}
즉 각각의 반복문안에서 하나의 스코프가 새롭게 바인딩된다. 즉 setTimeout
에서 출력되는 변수 i
는 각각이 다른 스코프를 가지고 있기 때문에 우리의 의도(?)대로 출력되지 않았다. 헤당 내용에 대한 스펙은 여기서 찾아볼 수 있다.
정리
var
대신에let
과const
를 사용하자. 서로가 모두 다르게 동작한다.let
을 사용해서for
반복문을 수행할 경우 각각의 반복마다 새로운 스코프가 바인딩된다.- 다 알고 있던 코드라도 조금씩 바꿔서 다시 공부해보자.