호이스팅

호이스팅(hoisting)에 대해 알아보자.

호이스팅(hoisting)

호이스팅은 선언한 함수와 변수를 자바스크립트가 해석할 때 가장 상단에 있는 것처럼 인식하는 것을 말한다.

좀 더 자세하게 말하자면 자바스크립트는 코드의 라인 순서와 관계없이 함수 선언식과 변수를 위한 메모리 공간을 먼저 확보한다. 따라서 함수와 변수(var)는 코드의 최상단으로 끌어 올려진 것(hoisted)처럼 보이게 되는데 이를 호이스팅이라고 한다.

  • 함수 전체가 호이스팅 된다.

  • var로 변수 선언시, 선언은 호이스팅되지만, 할당을 호이스팅 되지 않는다.

  • 변수 스코프는 함수이며, 함수만이 새 스코프를 도입할 수 있다.


호이스팅을 이해하기 위한 사전 지식들

호이스팅을 이해하기 위해 조금 축약하여 개념만 쉽게 이해해 보자.

스코프(scope)

스코프란 그 변수나 함수에 접근할 수 있는 위치를 말한다. 아래와 같은 예제가 있다면 x의 스코프는 함수 foo()이다.

function foo() {
  var x;
}

어휘적 스코프

어휘적 스코프는 정적 스코프(Static scope), 렉시컬 스코프(Lexical scope)라고도 불리며, 프로그램을 실행하지 않고 소스 코드에 존재하는대로 해석한 스코프이다.

변수 스코프

변수 스코프는 어휘적(정적)으로 지정되며, 즉 프로그램의 정적 구조를 보면 변수의 스코프를 판단할 수 있고 함수를 어디서 호출했는지 등에 영향을 받지 않는다.

또한 변수 스코프는 함수이며, 함수만이 새 스코프를 도입할 수 있다.

중첩 스코프

스코프가 변수의 스코프 안에 중첩되어 있으면 그 변수는 해당 스코프 전체에서 접근할 수 있다.

function foo(arg) {
  function bar() {
    console.log(`Hi, ${arg}`);
  }
  bar();
}

foo("jacob"); // Hi, jacob

Shadowing

쉐도잉란 내부 스코프에서 외부 스코프에 있는 변수와 이름이 같은 변수를 선언하면, 내부 스코프와 그 안에 중첩된 모든 스코프는 외부 스코프의 이름이 같은 변수에 접근할 수 있다.

내부 변수가 바뀌어도 외부 변수는 바뀌지 않으며, 내부 스코프에서 빠져나가면 다시 접근할 수 있다.

var x = "global";
function f() {
  var x = "local";
  console.log(x); // local
}
f(); // local
console.log(x); // global

var로 변수 선언시, 선언은 호이스팅되지만, 할당을 호이스팅 되지 않는다.

자바스크립트는 변수 선언을 모드 끌어올려 직접적 스코프 맨 앞으로 옮기며, 선언하기전 변수에 접근할 수 있게 된다. 물론 할당은 호이스팅되지 않기 때문에 초기화 전에 변수에 접근했다면 값은 undefined이다.

function f() {
  console.log(a); // undefined
  var a = 1;
  console.log(a); // 1
}

위 코드를 자바스크립트 엔진은 아래와 같이 실행한다.

function f() {
  var a; // 호이스팅!
  console.log(a); // undefined
  a = 1;
  console.log(a); // 1
}

가변 호이스팅

아래 예제를 예측해 보자.

var a = 123;

function f() {
  alert(a);
  var a = 1;
  alert(a);
}

f();

어떻게 결과가 나올까?

대부분 첫번째 alert창에서는 123이 나오고 두번째 alert창에서는 1이 나올거라고 예측할 수 있겠지만, 위에서 배운 내용을 토대로 살펴본다면 첫번째는 undeinfed, 두번째는 1이 나오게 된다.

변수의 스코프는 함수이기 때문에 var a = 1는 호이스팅되어, var a의 선언은 해당 함수(f())의 최 상단으로 이동하게 된다.

var a = 123;

function f() {
  var a;
  alert(a);
  a = 1;
  alert(a);
}

f();

외부 변수인 var a = 123은 쉐도잉이 되어, 내부 변수인(f()의 지역변수) var a;var a = 123;보다 우선되어, 첫번째 alert()에서는 undefined가 나오며, 두번째 alert()에서는 a=1로 초기화가 된 이후이기 때문에 1이 나오게 된다.


const, let

ES5를 아직 사용하고 있다면 위의 호이스팅의 동작을 주의 깊게 봐야한다. ES6에서는 변수를 선언하는 동안 추가 범위가 제공된다.

  • const, let은 블록 범위(block-scoped)이다.

블록이 뭔데요?

블록 범위라는 말은 많이 들어봤지만, java나 c, c++에 대한 배경 지식이 없다면 블록이라는게 생소할 수 있다.

var a = 1;
{
  let a = 2;
  console.log(a); // 2
}
console.log(a); //1

위 코드를 보면 줄괄호를 넣어놨는데, 저 중괄호 사이의 범위가 바로 블록이다. 위에서 말한 언어에서 범위를 정의하기 위해 블록을 도입했고, Javascript에서도 연결된 범위가 없기 때문에 관용적으로 블록을 도입하고 싶었다. 그래서 ES6에서 constlet 키워드를 통해 블록 범위 변수를 생성할 수 있도록 만들었다.

위 예를 보면 a블록 내부에 생성된 변수는 블록 내에서 사용할 수 있다. 일반적으로 블록 범위 변수를 선언할 때는 블록 상단에 선언을 추가하는 것이 좋다.

참고) TDZ

일시적 사각지대(TDZ, Temporal Dead Zone)는 블록 시작부터 초기화가 완료될 때까지의 위치를 말한다.

{
  // 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)
}

TDZ는 코드가 작성된 위치가 아닌 실행 순서에 따라 영역이 달라진다는 것이 중요하다.

{
  // TDZ starts at beginning of scope
  const func = () => console.log(letVar); /

  // Within the TDZ letVar access throws `ReferenceError`

  let letVar = 3; // End of TDZ (for letVar)
  func(); // Called outside TDZ!
}

위 코드와 같이 let 변수가 선언되기 전에 해당 변수를 사용하는 함수를 작성했더라도, TDZ 외부에서 해당 함수가 호출되었기 때문에 잘 동작하는 것을 볼 수 있다.

const, let

  • ES6에서 해당 키워드로 선언된 변수는 블록 범위로 호이스트 되지만, 선언 전에 변수를 참조하는 것은 오류로 된다.

  • const의 경우 값에 대한 읽기 전용 참조를 만든다. 이것은 참조가 보유한 값이 변경 불가능하다는 것을 의미 하진 않으며, 하지만 변수 식별자를 재할당 할 수는 없다.

참고) const나 let은 호이스팅 되는데 왜 사용할 수 없지?

이 부분은 궁금해서 v8의 코드를 조금 확인해 봤다. 물론 c++을 모르지만 테스트 코드를 통해서 해당 내용을 추측해 보자면 variableModevar라면 메모리에 할당시키지만, variableProxy에서 variableModelet이나 const라면 해당 위치를 초기화 해버려서 선언했어도 사용을 못하게 막아져 있었다.

Last updated