2장 블록 스코프 선언: let과 const

2023.06.16

2.1 let과 const 소개

  • var를 사용할 수 있는 모든 곳에서 let을 사용할 수 있다.
  • var와 마찬가지로 let은 초기화할 필요가 없다.
    • 이때 변수 값은 기본적으로 undefined 설정된다.
  • const는 상수를 선언한다.
  • 상수는 값이 변경될 수 없다는 점을 제외하면 변수와 같다.
    • 따라서 초기화를 해야 한다.
  • 상수에는 기본값이 없다.

2.2 진짜 블록 스코프

  • var로 블록 내에서 변수를 선언하면 해당 블록 내부뿐만 아니라 외부에서도 변수를 사용할 수 있다.
  • 이것은 아래와 같은 이유로 문제가 있다.
    1. 변수는 유지 관리를 위해 가능한 한 좁게 범위를 지정해야 한다.
    2. 코드의 명백한 의도와 실제 효과가 다를 때마다 버그와 유지 관리 문제를 일으킨다.
  • let과 const는 선언된 블록 내에서만 존재한다.
  • 이는 필요한 만큼만 존재하며 명백한 의도가 실제 효과와 일치한다.

2.3 반복된 선언은 에러다

  • var로 동일한 변수를 원하는 만큼 선언할 수 있다.
  • 변수를 두 번 이상 선언한다는 사실은 자바스크립트 엔진에서 완전히 무시된다.
  • 함수 전체에서 사용되는 단일 변수를 생성한다.
  • 이미 선언한 변수를 다시 선언하는 것은 아마도 실수일 것이다.
  • let과 const는 동일한 범위에서 반복 선언을 하면 오류가 발생한다.

2.4 호이스팅과 일시적 데드존

  • var 선언이 호이스팅된다는 것은 잘 알려져 있다.
  • let과 const를 사용하면 코드의 단계별 실행에서 선언이 처리될 때까지 변수를 사용할 수 없다.
  • 겉보기에는 let 선언은 var 선언처럼 함수의 맨 위로 올라가지 않는다. 하지만 이것은 흔히들 하 는 오해다. let과 const도 호이스팅된다. 단지 다르게 호이스팅될 뿐이다
  • let과 const는 임시 데드존(Temporal Dead Zone)이라는 개념을 사용한다.
  • 코드 실행 내에서 식별자를 전혀 사용할 수 없는 기간인 TDZ는 포함된 범위의 엔트리를 참조하는 데에 사용되지 않는다.
    1let answer; // 외부 'answer'
    2function notInitializedYet() {
    3  // 여기에 'answer'를 예약해 둔다.
    4  answer = 42; // ReferenceError: 'answer' is not defined
    5  console.log(answer);
    6  let answer; // 내부 'answer'
    7}
    8notInitializedYet();
  • TDZ는 코드 실행이 선언이 나타나는 범위에 들어갈 때 시작되고 선언이 실행될 때까지 계속된다.
  • TDZ는 공간적(위치 관련)이 아니라 시간적(시간 관련)이라는 점을 이해하는 것이 중요하다. 식별자를 사용할 수 없는 기간이다.
    1```
    2function temporalExample() {
    3  const f = () => {
    4    console.log(value);
    5  };
    6  let value = 42;
    7  f();
    8}
    9temporalExample();
    10```
  • 만약, TDZ가 공간에 관한 것이라면 value는 temporalExample의 맨 위의 코드 블록에서 사용될수 없고, 코드도 작동하지 않아야 한다.
  • 하지만 TDZ는 시간에 관한 것이며 f가 value를 사용하기 전에 선언이 실행되었으므로 문제가 없다.
  • TDZ는 함수에 적용되는 것과 마찬가지로 블록에도 적용이 된다.
    1function blockExample(str) {
    2  let p = 'prefix'; // The outer ' p ' declaration
    3  if (str) {
    4    p = p.toUpperCase(); // ReferenceError: 'p' is not defined
    5    str = str.toUpperCase();
    6    let p = str.indexOf('X'); // The inner ' p ' declaration
    7    if (p != -1) {
    8      str = str.substring(0, p);
    9    }
    10  }
    11  return p + str;
    12}
    13blockExample('Test X');
  • 블록 내부의 첫 번째 줄에는 p를 사용할 수 없다. 왜나하면 함수에서 선언되었더라도 p 식별자의 소유권을 갖는 블록 내부에 섀도잉(shadowing) 선언이 있기 때문이다. 따라서, 식별자는 let 선언이 실행된 후에만 새로운 내부 p를 참조할 수 있다.

2.5 새로운 종류의 전역(global)

1var answer = 42;
2console.log("answer == " + answer);
3console.log("this.answer == " + this.answer);
4console.log("has property? " + ("answer" in this));
5
6answer == 42
7this.answer == true
8has property? false
1let answer = 42;
2console.log("answer == " + answer);
3console.log("this.answer == " + this.answer);
4console.log("has property? " + ("answer" in this));
5
6answer == 42
7this.answer == undefined
8has property? false
  • let과 const를 사용하면 전역 변수를 생성하지만 전역 변수는 전역 객체의 속성이 아니다.

2.6 const: 자바스크립트의 상수

2.6.1 const 기초

  • 당연하게도 상수에 새 값을 할당할 수 없지만 변수에 새 값을 할당할 수 없다는 점을 제외하면 동일한 범위 규칙, 임시 데드존 등 모든 것은 let으로 변수를 만드는 것과 똑같다.
  • 상수에 새 값을 할당하려고 하면 TypeError가 가 발생한다.

2.6.2 const가 참조하는 객체는 여전히 변경 가능

  • 상수의 값이 객체 참조 라면 어떤 식으로든 객체가 변경 불가능하다는 것(상태를 변경할 수 없음)을 의미하지는 않는다. 객체는 여전히 변경 가능하다. 이는 상숫값을 변경하기 때문에 다른 객체에 대한 상수 지점을 만들 수 없음을 의미한다.

2.7 루프의 블록 스코프

2.7.1 “루프 내 클로저” 문제

1function closuresInLoopsProblem() {
2  for (var counter = 1; counter <= 3; ++counter) {
3    setTimeout(function () {
4      console.log(counter);
5    }, 10);
6  }
7}
8closuresInLoopsProblem();
  • 코드가 1, 2, 3을 출력할 것으로 예상했을 텐데 실제로는 4, 4, 4를 출력한다. 그 이유는 루프가 끝날 때까지 각 타이머가 콜백을 실행하지 않기 때문이다.
1function closuresInLoopsES5() {
2  for (var counter = 1; counter <= 3; ++counter) {
3    (function (value) {
4      setTimeout(function () {
5        console.log(value);
6      }, 10);
7    })(counter);
8  }
9}
10closuresInLoopsES5();
  • ES5와 이전 버전에서는 다른 함수를 도입하고 counter를 인수로 전달한 다음 해당 인수를 사용하는 것이다.
1function closuresInLoopsWithLet() {
2  for (let counter = 1; counter <= 3; ++counter) {
3    setTimeout(function () {
4      console.log(counter);
5    }, 10);
6  }
7}
8closuresInLoopsWithLet();
  • ES2015의 let으로 변경하면 더 간단히 해결 할 수 있다.

2.7.2 바인딩: 변수, 상수, 기타 식별자의 작동 방식

  • const는 범위, 보유할 수 있는 값의 종류 등의 관점에서 let과 동일하게 동작한다는 것을 배웠다.
  • 내부적으로는 변수와 상수는 사양에서 바인딩(특히 이 경우 식별자 바인딩)이라고 부르는 동일한 것이다.
  • 변수는 변경 가능한 바인딩을, 상수는 변경할 수 없는 바인딩을 만든다.IMG_0337.jpg
    1function closuresInLoopsWithLet() {
    2  for (let counter = 1; counter <= 3; ++counter) {
    3    setTimeout(function () {
    4      console.log(counter);
    5    }, 10);
    6  }
    7}
    8closuresInLoopsWithLet();
  • 타이머 함수가 타이머에 의해 호출될 때 각각 별도의 환경 객체를 사용하고 각각 자체 counter 복사본을 사용하기 때문에 동일한 환경 객체와 변수를 모두가 공유하는 var의 값인 4, 4, 4대신 1, 2, 3이 표시된다.
  • 요컨대, 루프의 블록 스코프 메커니즘은 ES5 솔루션의 익명 함수가 수행한 작업을 정확히 수행했다.
  • 각 타이머 함수에 바인딩의 자체 복사본으로 감쌀 수 있는 다른 환경 객체를 제공했다. 그러나 별도의 함수와 함수 호출없이 더 효율적으로 수행했다.

2.7.3 while과 do-while 루프

  • while과 do-while은 블록이 자체 환경 객체를 가진다는 사실에서 오는 이점도 있다.
  • for의 초기화 표현식이 없기 때문에 거기에 선언된 바인딩 값을 복사하는 작업을 수행하지 않지만 각 루프 반복과 연관된 블록은 여전히 자체 환경을 갖는다.
1function closuresInWhileLoops() {
2  let outside = 1;
3  while (outside <= 3) {
4    let inside = outside;
5    setTimeout(function () {
6      console.log('inside = ' + inside + ', outside = ' + outside);
7    }, 10);
8    ++outside;
9  }
10}
11closuresInWhileLoops();
12
13// inside = 1, outside = 4
14// inside = 2, outside = 4
15// inside = 3, outside = 4

2.7.4 성능 영향

  • 루프에서 블록 스코프가 작동하는 방식에 대해 생각하면 다음과 같이 생각할 수 있다.

루프에서 블록 변수를 사용하고 이를 보유하고 체인을 설정하고 (for 루프의 경우) 복사할 새 환경 객체를 만들어야 하고 어딘가에서 어딘가로 반복 바인딩 값을 복사해야 한다면 루프 속도가 느려지지 않을까?

  • 이에 대한 두 가지 답변이 있다.
    1. 아마 상관하지 않을 것이다. 성급한 최적화는 성급하다는 것을 기억하자. 실제 성능 문제가 있어 해결해야 하는 경우에 걱정하자.
    2. 그렇기도 하고 아니기도 하다. 자바스크립트 엔진이 차이를 최적화하지 않았고 차이를 최적화할 수 없는 경우(루프의 클로저 예 포함)가 있다면 확실히 더 많은 오버 헤드가 발생한다. 다른 경우에는 클로저를 생성하지 않거나 엔진이 클로저가 루프 반복 변수를 사용하지 사용하면 않는다고 결정할 수 있다면 차이를 최적화할 수 있다. 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와 같은 흐름 제어문에 연결될 필요가 없다.
    • 독립적으로 사용할 수 있다.
    • 범위 지정 기능은 범위 지정 블록이 될 수 있다.