[JS] Scope & Hoisting ( var, let, const )

colki·2021년 3월 11일
0

Scope

코드의 접근 범위를 결정하는 것을 말하는데, 이는 자바스크립트가 식별자를 찾아내기 위한 규칙이라고 생각하면 된다. 모든 변수는 스코프를 가지고 있고 크게 2가지로 분류할 수 있다.

◾ 전역 스코프 (Global scope)
코드 어디에서든지 참조할 수 있다.
전역에서 선언한 변수를 전역변수라고 한다.

◾ 지역 스코프 (Local scope or Function-level scope)
함수 코드 블록이 만든 스코프로 함수 자신과 하위 함수에서만 참조할 수 있다.
지역에서 선언한 변수를 지역변수라고 한다.

전역에 선언된 변수는 어디에든 참조할 수 있지만,
함수 내에서 선언된 변수는 오직 함수안에서만 사용할 수 있고 외부에서는 접근할 수 없다.
이러한 규칙을 스코프라고 한다.

스코프는 변수의 선언 위치를 기준으로 결정되며 또한,
Scope Chain구조로 계층적으로 연결되어 있다.
자바스크립트는 어디서 호출하는지가 아니라 어디에 선언하였는지에 따라 결정하는 렉시컬스코프를 따르기 때문에 함수가 호출이 되면, 최하위 스코프부터 최상위의 정보까지 접근하여 원하는 변수의 값을 찾는다.

함수 레벨스코프와 블록레벨스코프에 대해서도 알아보자.


# 함수레벨스코프

자바스크립트는 기본적으로 함수스코프를 기준으로 하는 함수레벨스코프를 따른다.
함수 레벨 스코프란 함수 코드 블록 내에서 선언된 변수는 함수 코드 블록 내에서만 유효하고 함수 외부에서는 참조할 수 없는 스코프규칙이다.

예제(1)

function foo() {
  for (var j = 0; j < 3; j++) {
   console.log(`inner: ${j}`); 
  }
  console.log(`outter: ${j}`);
}

foo();

// console 
"inner: 0"
"inner: 1"
"inner: 2"
"outter: 3"

var는 함수스코프레벨을 따르므로 for문의 코드블록이 종료된 다음의 i값을 받아올 수 있다.
for문 안에서는 0, 1, 2출력되지만 i가 3일 때 종료되기때문에 for문 밖에서는
i는 3으로 할당된다. 그래서 outter 값에는 3이 출력된다.

예제 (2) 

function foo () {
  var a = 5;
  for (var i = 0; i < a; i++) {
    console.log(a);//5 5 5 5 5
  }
  console.log(i);  // 5
}
foo();

var는 자신이 속한 함수안에서는 코드블록 안에 있든 없든 무조건 전역변수로 사용된다.

for문 안에서는 전역변수의 값을 받아올 수 있기 때문에 a의 값은 5이다.
for문 밖에서는 예제(1)에서 언급했던 대로 for문은 i가 5일때 false이기 때문에 종료되므로
두 번째 콘솔에는 5가 출력된다.



# 블록레벨스코프

다른 프로그래밍 언어는 블록 레벨 스코프(block-level scope)를 따르는데,
블록 레벨 스코프란 코드 블록({…})내에서만 변수를 사용하는 스코프를 의미한다.
그런데 자바스크립트에서도 var가 아닌
let, const 키워드를 사용하면 블록 레벨스코프를 따르게 된다.

같은 예제로 차이에 대해서 알아보겠다.


🔸 var 함수레벨스코프

function sayHello () {
  if(true){
  var say = 'hello'
  console.log(say); // hello
  }

  console.log(say); // hello
}

함수블록문 기준으로 스코프가 결정되므로 var say;는 sayHello함수 내에서 전역변수로 선언된다.
if코드블록문 밖에 있는 콘솔역시 함수내부에 있으므로 전역변수의 값을 참조하여 hello를 출력하게 된다.


🔹const 블록레벨스코프

function sayHello () {
  if(true){
  const say = 'hello'
  console.log(say); // hello
  }

  console.log(say);
  // say is not defined at sayHello
}

sayHello();

코드블록문을 기준으로 스코프가 결정되므로, 여기서 if문은 지역스코프가 되고 상대적으로 sayHello함수스코프는 전역이 된다.
전역에 있는 console.log(say);는 지역에 있는 say변수에 접근할 수 없으므로 선언된 적 없는 변수에 대한 에러가 출력된다.


🔹 let 블록레벨스코프

* 위에서 var로 다뤘던 구문을 똑같이 가져왔다*
function foo () {
  let a = 5;
  for (let i = 0; i < a; i++) {
    console.log(a); // 5
  }
  console.log(i);
  // i is not defined at foo
}

foo();

이번에는 어떤 값이 출력되었는가.
let은 var와 달리 블록레벨스코프를 따르기 때문에 for문이 지역변수, foo 함수스코프가 전역이 된다. for문의 코드블록문 옆에 있는 조건식도 코드블록같이 취급한다. 그래서 foo함수(전역)에선 지역에 해당하는 for문에 접근할 수 없기 때문에 함수내부에서 i라는 변수를 찾지 못한다.

전역스코프 (글로벌스코프)


  <body>
    <h1>Hello World</h1>
    <script>
      var yummy = '떡볶이';

      console.log(`${yummy}는 맛있어!`);
      // 떡볶이는 맛있어!
    </script>
    <script>
      var yummy = '건포도';

      console.log(`${yummy}는 맛있어!`);
      // 건포도는 맛있어!
    </script>
  </body>

하나의 html(웹페이지)에는 운동장이 하나라,
스크립트 학생들이 사실 같은 공간에서 뛰어 놀고 있는 것이다.

◼ 운동장처럼 공유하는 공간을 전역스코프=글로벌스코프라고 한다.
그래서 위에서처럼 마지막에서는 yummy 변수에는 '건포도'라는 값이 할당되고
건포도는 맛있어!... 라는 말도 안되는 참혹한 문장이 출력된다.

나의 소중한 변수를 공용공간(Global Scope)에 배치해둔다면, 다른 누군가가
의도치않게 그 변수를 사용할 수도 있고 혹은 수정/삭제할 가능성도 있기 때문에
꼭 필요한 경우가 아니라면 전역변수는 지양하는 것이 좋다.

그에 반해 지역스코프, 지역변수는 나만의 공간이니 소중한 내 변수를 보호할 수 있는 울타리가 되어 준다.


◼ var를 이용하여 선언한 전역 변수는 전역 객체의 Key/Value로 추가되지만, let을 이용하여 선언한 전역 변수는 이와 같지 않다.

var a = "global";
let b = "global";

console.log(window.a); // "global"
console.log(window.b); // undefined

암묵적 전역
(선언하지 않은 식별자)

var a = 10;

function foo () {
  y = 20; 
  console.log(x + y);
}

foo(); // ?

함수스코프(지역스코프)안에 var, let, const등이 붙지 않고 값이 대입된
y = 20;이라는 문장이 있다.
위 예제에서 콘솔엔 어떤 값이 출력될까?

foo 함수가 호출되면 자바스크립트 엔진은 변수 y에 값을 할당하기 위해 먼저 스코프 체인을 통해 선언된 변수인지 확인한다.

이때 foo 함수의 스코프와 전역 스코프 어디에서도 변수 y의 선언을 찾을 수 없으므로 참조 에러가 발생해야 하지만 자바스크립트 엔진은 y = 20을 window.y = 20으로 해석하여 프로퍼티를 동적 생성한다.

결국 y는 전역 객체(window)의 프로퍼티가 되어 마치 전역 변수처럼 동작한다.

이러한 현상을 암묵적 전역(implicit global)이라 한다.

var a = 10; // 전역 변수

function foo () {
  
  y = 20; 
  선언하지 않은 식별자
  // 전역 객체의 프로퍼티가 된다.
  console.log(x + y);
}

foo(); // 30

y는 변수 선언없이 단지 전역 객체(window)의 프로퍼티로 추가되었을 뿐.

따라서 y는 변수가 아니다. 그러므로 변수가 아닌 y는 변수 호이스팅이 발생하지 않는다.

또한 변수가 아니라 단지 프로퍼티인 y는 delete 연산자로 삭제할 수 있다.
그에 반해 전역 변수는 프로퍼티이지만 delete 연산자로 삭제할 수 없다.


Hoisting

자바스크립트에서 우리가 사용하는 모든 변수 선언문자신이 속한스코프 내에서 최상위로 Hoisting 된다.

변수 "선언"이 있어야 호이스팅이 일어난다.
호이스팅 또한 변수 앞에 오는 키워드가 var 인지 let, const 인지에 따라 결과가 다르게 나오는데 먼저 var부터 살펴보겠다.

var 의 Hoisting

console.log(a);
var a = 1;

var a 까지가 변수선언문이고,
=1은 값을 대입하는 역할

var a; hoisting
console.log(a); // undefined
a = 1;

var a;만 먼저 hoisting으로 끌어올려지기 때문에
콘솔에서는 값이 할당되지 않은 a를 undefined로 출력한다.
그리고 그다음에a=1 값을 대입하지만 이미 콘솔기차는 떠나간 후이다.

var a = 1;

function foo () {
  console.log(a); // ???
  var a = 2;
}

foo();

함수스코프에서도 똑같은 현상이 발생한다.

var a = 1;


function foo () {
  var a;  // 초기화가 이루어진다. var a = undefined;와 같다.
  console.log(a); // undefined
  a = 2;

이 함수 내부는 또 다른 전역인 세상?인 것이다.
}

foo();

전역의 a값을 먼저 찾기전에 함수스코프 내에서 호이스팅
var a;를 보고 undefined로 바로 규정해버린다.

let, const 의 Hoisting

//콘솔에 출력될값은?

console.log(foo); 
var foo;

console.log(bar); 
let bar;
-
둘 다 undefined 아냐?
반은 맞고 반은 틀리다.
-
console.log(foo); // undefined
var foo;

console.log(bar); 
// Error: Uncaught ReferenceError: bar is not defined
let bar;

대충 맞춘 것 같은데, undefinederror 뭐가 다른 걸까?
error 내용을 보면not defined라고 되어있어서 비슷한 것같기도 하고..!? 이유를 알려고 하지 않고 이럴땐 이렇구나 암기식으로 넘어가려 했던 내 자신을 반성하게 됐다. 이유가 다 있구나.


호이스팅 단계에 대해 자세히 알아보면 차이를 알 수 있다.

선언 단계(Declaration phase)
변수를 실행 컨텍스트의 변수 객체(Variable Object)에 등록한다. 이 변수 객체는 스코프가 참조하는 대상이 된다.

초기화 단계(Initialization phase)
변수 객체(Variable Object)에 등록된 변수를 위한 공간을 메모리에 확보한다. 이 단계에서 변수는 undefined로 초기화된다.

할당 단계(Assignment phase)
undefined로 초기화된 변수에 실제 값을 할당한다. 신기하다😮

🔸 var 호이스팅

var 키워드로 선언된 변수는 선언 단계와 초기화 단계가 한번에 이루어진다. 변수를 등록(선언 단계)한 순가 메모리에 변수를 위한 공간을 확보한 후, undefined로 초기화(초기화 단계)한다.

따라서 변수 선언문 이전에 변수에 접근하여도 스코프에 undefined로 초기화되어 있으므로 undefined를 반환한다. 이후 변수 할당문에 도달하면서 비로소 값이 할당된다.

// 스코프의 선두에서 선언 단계와 초기화 단계가 실행된다.
// 따라서 변수 선언문 이전에 변수를 참조할 수 있다. 
* var foo; 아 foo이거 물어본거지? 값없는데? 😮 *
console.log(foo); // undefined

var foo;
console.log(foo); // undefined

foo = 1; // 할당문에서 할당 단계가 실행된다.
console.log(foo); // 1

🔹 let, const 호이스팅

let 키워드로 선언된 변수는 선언 단계와 초기화 단계가 분리되어 진행된다.
변수를 등록(선언단계)하지만 초기화 단계는 변수 선언문이 실행된 다음 이루어진다.
초기화 이전에 변수에 접근하려고 하면 참조 에러(ReferenceError)가 발생하는데 메모리 공간을 아직 확보되지 않았기 때문이다. (메모리확보 = 초기화)

따라서 스코프의 시작 지점부터 초기화 시작 지점까지는 변수를 참조할 수 없다.
초기화가 안되는 이 구간을 TDZ(temporal dead zone: 일시적사각지대)라고 부른다.

// 스코프의 선두에서 선언 단계가 실행된다.
// 아직 변수가 초기화(메모리 공간 확보와 undefined로 초기화)되지 않았다.
// 따라서 변수 선언문 이전에 변수를 참조할 수 없다. 
// var 였다면 콘솔로그 위에 var foo; 이렇게 초기화가 먼저 됐을 것이다.

*? foo? 본적은 있는 것 같은데 뭐하는애지?먹는거야? 🤔 *

console.log(foo); // ReferenceError: foo is not defined
<               TMZ            >

let foo; 
// let foo;위의 < >구간이 TMZ
// 여기서초기화 단계가 실행된다.

console.log(foo); // undefined

foo = 1; // 할당문에서 할당 단계가 실행된다.
console.log(foo); // 1


let과 const의 차이
let은 재할당이 자유로우나 const는 재할당이 금지된다.
기본적으로는 재할당하는 경우는 드물기 때문에, 기본적으로는 const를 사용하자.
혹여 재할당이 반드시 필요하다면, 그때가서 const를 let 키워드로 변경해도 결코 늦지 않는다.



함수표현식 vs 함수선언식

변수에 함수를 대입하는 함수표현식 또한 호출하려면 먼저 선언이 되어야 한다.

sayMyName(); ---?

var sayMyName = function () {
  console.log('I am hany');
};

sayMyName(); ---?

여기서 sayMyName 함수를 호출할 수 있는 올바른 위치는?
뒤에 함수가 있어 헷갈릴 수 있지만 var colki;먼저 hoisting되는 건 똑같다!

var sayMyName;
  
sayMyName(); 
// sayMyName => undefined이니까 실행할수 없다.
// sayMyName is not a function

sayMyName = function () {
  console.log('I am hany');
};

sayMyName(); // I am hany

함수 표현식으로 함수를 작성하는 경우에는 항상 실행문의 위치와 함수를 대입하는
구문의 위치가 중요하다. "선언 먼저 실행은 나중에"
현업에서도 함수선언식으로 대부분 사용한다고 하지만 차이에 대해서는 제대로 알고 있어야 한다.

Reference PoiemaWeb

profile
매일 성장하는 프론트엔드 개발자

0개의 댓글