2장 블록 스코프 선언: let과 const
2023.06.16
2.1 let과 const 소개
- var를 사용할 수 있는 모든 곳에서 let을 사용할 수 있다.
- var와 마찬가지로 let은 초기화할 필요가 없다.
- 이때 변수 값은 기본적으로 undefined 설정된다.
- const는 상수를 선언한다.
- 상수는 값이 변경될 수 없다는 점을 제외하면 변수와 같다.
- 상수에는 기본값이 없다.
2.2 진짜 블록 스코프
- var로 블록 내에서 변수를 선언하면 해당 블록 내부뿐만 아니라 외부에서도 변수를 사용할 수 있다.
- 이것은 아래와 같은 이유로 문제가 있다.
- 변수는 유지 관리를 위해 가능한 한 좁게 범위를 지정해야 한다.
- 코드의 명백한 의도와 실제 효과가 다를 때마다 버그와 유지 관리 문제를 일으킨다.
- let과 const는 선언된 블록 내에서만 존재한다.
- 이는 필요한 만큼만 존재하며 명백한 의도가 실제 효과와 일치한다.
2.3 반복된 선언은 에러다
- var로 동일한 변수를 원하는 만큼 선언할 수 있다.
- 변수를 두 번 이상 선언한다는 사실은 자바스크립트 엔진에서 완전히 무시된다.
- 함수 전체에서 사용되는 단일 변수를 생성한다.
- 이미 선언한 변수를 다시 선언하는 것은 아마도 실수일 것이다.
- let과 const는 동일한 범위에서 반복 선언을 하면 오류가 발생한다.
2.4 호이스팅과 일시적 데드존
- var 선언이 호이스팅된다는 것은 잘 알려져 있다.
- let과 const를 사용하면 코드의 단계별 실행에서 선언이 처리될 때까지 변수를 사용할 수 없다.
- 겉보기에는 let 선언은 var 선언처럼 함수의 맨 위로 올라가지 않는다. 하지만 이것은 흔히들 하 는 오해다. let과 const도 호이스팅된다. 단지 다르게 호이스팅될 뿐이다
- let과 const는 임시 데드존(Temporal Dead Zone)이라는 개념을 사용한다.
- 코드 실행 내에서 식별자를 전혀 사용할 수 없는 기간인 TDZ는 포함된 범위의 엔트리를 참조하는 데에 사용되지 않는다.
let answer; // 외부 'answer'
function notInitializedYet() {
// 여기에 'answer'를 예약해 둔다.
answer = 42; // ReferenceError: 'answer' is not defined
console.log(answer);
let answer; // 내부 'answer'
}
notInitializedYet();
- TDZ는 코드 실행이 선언이 나타나는 범위에 들어갈 때 시작되고 선언이 실행될 때까지 계속된다.
- TDZ는 공간적(위치 관련)이 아니라 시간적(시간 관련)이라는 점을 이해하는 것이 중요하다.
식별자를 사용할 수 없는 기간이다.
```
function temporalExample() {
const f = () => {
console.log(value);
};
let value = 42;
f();
}
temporalExample();
```
- 만약, TDZ가 공간에 관한 것이라면 value는 temporalExample의 맨 위의 코드 블록에서 사용될수 없고, 코드도 작동하지 않아야 한다.
- 하지만 TDZ는 시간에 관한 것이며 f가 value를 사용하기 전에 선언이 실행되었으므로 문제가 없다.
- TDZ는 함수에 적용되는 것과 마찬가지로 블록에도 적용이 된다.
function blockExample(str) {
let p = 'prefix'; // The outer ' p ' declaration
if (str) {
p = p.toUpperCase(); // ReferenceError: 'p' is not defined
str = str.toUpperCase();
let p = str.indexOf('X'); // The inner ' p ' declaration
if (p != -1) {
str = str.substring(0, p);
}
}
return p + str;
}
blockExample('Test X');
- 블록 내부의 첫 번째 줄에는 p를 사용할 수 없다. 왜나하면 함수에서 선언되었더라도 p 식별자의 소유권을 갖는 블록 내부에 섀도잉(shadowing) 선언이 있기 때문이다.
따라서, 식별자는 let 선언이 실행된 후에만 새로운 내부 p를 참조할 수 있다.
2.5 새로운 종류의 전역(global)
var answer = 42;
console.log("answer == " + answer);
console.log("this.answer == " + this.answer);
console.log("has property? " + ("answer" in this));
answer == 42
this.answer == true
has property? false
let answer = 42;
console.log("answer == " + answer);
console.log("this.answer == " + this.answer);
console.log("has property? " + ("answer" in this));
answer == 42
this.answer == undefined
has property? false
- let과 const를 사용하면 전역 변수를 생성하지만 전역 변수는 전역 객체의 속성이 아니다.
2.6 const: 자바스크립트의 상수
2.6.1 const 기초
- 당연하게도 상수에 새 값을 할당할 수 없지만 변수에 새 값을 할당할 수 없다는 점을 제외하면 동일한 범위 규칙, 임시 데드존 등 모든 것은 let으로 변수를 만드는 것과 똑같다.
- 상수에 새 값을 할당하려고 하면 TypeError가 가 발생한다.
2.6.2 const가 참조하는 객체는 여전히 변경 가능
- 상수의 값이 객체 참조 라면 어떤 식으로든 객체가 변경 불가능하다는 것(상태를 변경할 수 없음)을 의미하지는 않는다.
객체는 여전히 변경 가능하다.
이는 상숫값을 변경하기 때문에 다른 객체에 대한 상수 지점을 만들 수 없음을 의미한다.
2.7 루프의 블록 스코프
2.7.1 “루프 내 클로저” 문제
function closuresInLoopsProblem() {
for (var counter = 1; counter <= 3; ++counter) {
setTimeout(function () {
console.log(counter);
}, 10);
}
}
closuresInLoopsProblem();
- 코드가 1, 2, 3을 출력할 것으로 예상했을 텐데 실제로는 4, 4, 4를 출력한다.
그 이유는 루프가 끝날 때까지 각 타이머가 콜백을 실행하지 않기 때문이다.
function closuresInLoopsES5() {
for (var counter = 1; counter <= 3; ++counter) {
(function (value) {
setTimeout(function () {
console.log(value);
}, 10);
})(counter);
}
}
closuresInLoopsES5();
- ES5와 이전 버전에서는 다른 함수를 도입하고 counter를 인수로 전달한 다음 해당 인수를 사용하는 것이다.
function closuresInLoopsWithLet() {
for (let counter = 1; counter <= 3; ++counter) {
setTimeout(function () {
console.log(counter);
}, 10);
}
}
closuresInLoopsWithLet();
- ES2015의 let으로 변경하면 더 간단히 해결 할 수 있다.
2.7.2 바인딩: 변수, 상수, 기타 식별자의 작동 방식
- const는 범위, 보유할 수 있는 값의 종류 등의 관점에서 let과 동일하게 동작한다는 것을 배웠다.
- 내부적으로는 변수와 상수는 사양에서 바인딩(특히 이 경우 식별자 바인딩)이라고 부르는 동일한 것이다.
- 변수는 변경 가능한 바인딩을, 상수는 변경할 수 없는 바인딩을 만든다.
function closuresInLoopsWithLet() {
for (let counter = 1; counter <= 3; ++counter) {
setTimeout(function () {
console.log(counter);
}, 10);
}
}
closuresInLoopsWithLet();
- 타이머 함수가 타이머에 의해 호출될 때 각각 별도의 환경 객체를 사용하고 각각 자체 counter 복사본을 사용하기 때문에 동일한 환경 객체와 변수를 모두가 공유하는 var의 값인 4, 4, 4대신 1, 2, 3이 표시된다.
- 요컨대, 루프의 블록 스코프 메커니즘은 ES5 솔루션의 익명 함수가 수행한 작업을 정확히 수행했다.
- 각 타이머 함수에 바인딩의 자체 복사본으로 감쌀 수 있는 다른 환경 객체를 제공했다.
그러나 별도의 함수와 함수 호출없이 더 효율적으로 수행했다.
2.7.3 while과 do-while 루프
- while과 do-while은 블록이 자체 환경 객체를 가진다는 사실에서 오는 이점도 있다.
- for의 초기화 표현식이 없기 때문에 거기에 선언된 바인딩 값을 복사하는 작업을 수행하지 않지만 각 루프 반복과 연관된 블록은 여전히 자체 환경을 갖는다.
function closuresInWhileLoops() {
let outside = 1;
while (outside <= 3) {
let inside = outside;
setTimeout(function () {
console.log('inside = ' + inside + ', outside = ' + outside);
}, 10);
++outside;
}
}
closuresInWhileLoops();
// inside = 1, outside = 4
// inside = 2, outside = 4
// inside = 3, outside = 4
2.7.4 성능 영향
- 루프에서 블록 스코프가 작동하는 방식에 대해 생각하면 다음과 같이 생각할 수 있다.
루프에서 블록 변수를 사용하고 이를 보유하고 체인을 설정하고 (for 루프의 경우) 복사할 새 환경 객체를 만들어야 하고 어딘가에서 어딘가로 반복 바인딩 값을 복사해야 한다면 루프 속도가 느려지지 않을까?
- 이에 대한 두 가지 답변이 있다.
- 아마 상관하지 않을 것이다.
성급한 최적화는 성급하다는 것을 기억하자. 실제 성능 문제가 있어 해결해야 하는 경우에 걱정하자.
- 그렇기도 하고 아니기도 하다.
자바스크립트 엔진이 차이를 최적화하지 않았고 차이를 최적화할 수 없는 경우(루프의 클로저 예 포함)가 있다면 확실히 더 많은 오버 헤드가 발생한다.
다른 경우에는 클로저를 생성하지 않거나 엔진이 클로저가 루프 반복 변수를 사용하지 사용하면 않는다고 결정할 수 있다면 차이를 최적화할 수 있다.
V8의 엔지니어가 이를 최적화하는 방법을 찾았기 때문에 (클로저가 생성되지 않는 경우 등) 속도 차이는 사라졌다.
2.7.5 루프 블록에서 const
- 블록 내의 값이 절대 변경되지 않는다면, 범위 내에서 상수이며 const를 선언하여 의도를 표시할 수 있다.
2.7.6 for-in 루프에서 const
- for-in 루프에서도 let이나 const를 사용할 수 있다.
- 어휘 선언이 있는 for-in 루프는 while처럼 각 루프 반복에 대해 별도의 환경 객체를 얻는다.
2.8 과거 습관을 새롭게
2.8.1 var 대신 const 또는 let 사용
- const나 let을 사용하자.
- 변경하지 않으려는 “변수”에 const를 채택하여 사용하면 실제 변수는 얼마 되지 않는다는 점에 놀랄 것이다.
2.8.2 변수 범위를 좁게 유지
- 가장 좁은 범위에서 let과 const를 사용하자.
- 코드의 유지 보수성을 향상시킨다.
2.8.3 인라인 익명 함수 대신 블록 스코프 사용
- 루프 내 클로저 문제의 경우 블록 스코프를 사용하면 코드가 훨씬 깨끗하고 읽기 쉽다.
- 블록은 if나 for와 같은 흐름 제어문에 연결될 필요가 없다.
- 독립적으로 사용할 수 있다.
- 범위 지정 기능은 범위 지정 블록이 될 수 있다.