[JavaScript] scope

Scope

변수가 중복 선언되었을 때, 어떤 식으로 변수를 식별할까

스코프는 참조 대상 식별자를 찾아내기 위한 규칙이다. 식별자는 어디에 선언되었는지에 따라 유효한 범위를 가질 수 있다.

스코프의 구분

  • 전역 스코프 : 코드 어디에서든지 참조 가능
  • 지역 스코프 : 함수 코드 블록이 만든 스코프, 함수 자신과 하위 함수에서만 참조 가능

  • 전역 변수 : 전역에서 선언된 변수, 어디에서든 참조 가능
  • 지역 변수 : 지역(함수) 내에서 선언된 변수, 위와 같다.

자바스크립트 스코프의 특징

C언어는 블록 레벨 스코프를 따른다. 하지만 자바스크립트는 함수 레벨 스코프를 따른다.

함수 레벨 스코프란, 함수 코드 블록에서 선언된 변수는 함수 코드 블록 내에서만 유효하고 함수 외부에서는 유효하지 않은 것이다.(참조할 수 없다.)

ES6에서 도입된 let을 사용하면 블록 레벨 스코프를 사용할 수 있다.

let y = 0;
{
  let y = 1;
  console.log(y); // 1
}
console.log(y);   // 0

전역 스코프

전역에 변수를 선언하면 전역 스코프를 갖는 전역 변수가 된다. var 키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티이다.

전역 변수의 사용을 변수 이름이 중복될 수 있고, 이로 인한 의도치 않은 재할당으로 코드의 변화를 일으킨다. 그렇게 코드를 예측하기 어렵게 만드므로 사용을 최소화하여야 한다.

비블록 레벨 스코프(Non block-level scope)

자바스크립트는 블록 레벨 스코프를 사용하지 않으므로, 코드 블록 내에서 선언되었더라도 함수 밖에서 선언되었다면 모두 전역 스코프를 갖게 된다.

if (true) {
  var x = 5;
}
console.log(x);

함수 레벨 스코프

자바스크립트는 함수 레벨 스코프를 사용한다. 그래서 함수 내에서 선언된 매개변수 및 변수는 함수 외부에서는 참조할 수 없다. 즉, 함수 내의 변수는 지역 변수이다.

전역 변수와 지역 변수가 중복 선언되었을 때, 지역 변수를 우선하여 참조한다.

이를 활용하여 내부 함수는 자신을 포함하고 있는 외부 함수의 변수에 접근할 수 있다. 즉, 함수 영역에서도 전역 변수를 참조할 수 있고, 전역 변수의 값을 변경할 수도 있다. 즉, 상위 함수에서 선언한 변수를 접근 / 변경이 가능하다.

전역변수로 처음에 할당된 변수가 후에 함수 내부에서 재할당되었을 때, 실행 컨텍스트의 스코프 체인에 의해 참조 순위에서 전역 변수로 할당됬던 변수가 뒤로 밀리게 된다.

렉시컬 스코프

특정 함수를 실행하여 값이 나올 때, 상위 스코프가 무엇인지에 따라 결정되는 경우가 있다. 그럴 때 두 가지 패턴을 통해 예측할 수 있는데, 함수를 어디서 호출하였는지, 함수를 어디서 선언하였는지에 따라 상위 스코프를 결정할 수 있다.

함수 호출 위치 방식으로 상위 스코프를 결정하게 되는 것을 동적 스코프라고 하고, 함수 선언 위치에 따라서 상위 스코프를 결정하는 경우를 렉시컬 스코프(정적 스코프)라고 한다. 자바스크립트를 포함한 대부분의 언어가 정적 스코프를 따른다.

즉, 렉시컬 스코프는 함수를 어디에 선언하였는지에 따라서 실행할 상위 스코프를 결정한다.

image

암묵적 전역 변수

함수 내에 선언되지 않은 변수에 값을 할당하게되면, 해당 변수의 참조를 찾아야 값을 할당할 수 있기 때문에 자바스크립트 엔진에서 스코프 체인에 변수를 검색한다.

자바스크립트 엔진은 먼저 함수 내의 스코프에서 변수를 검색한다. 해당 변수에 대한 변수 선언이 없으므로 해당 함수의 상위 컨텍스트에서 변수를 다시 검색한다.(전역 스코프나 상위 함수)

전역 스코프에도 해당 변수의 선언이 존재하지 않으면, 자바스크립트 엔진은 해당 변수의 선언을 암묵적으로 해버리고 값을 할당한다. 즉, 키워드를 생략하고 값을 할당한 변수는 암묵적으로 전역 변수가 된다.

Screen Shot 2019-12-01 at 1 39 43 PM

암묵적 전역 변수는 오류의 발생 원인이 되기 때문에 변수 선언시 반드시 키워드를 사용하여야 한다.

변수 이름의 중복

전역 변수를 사용할 이유가 없다면 지역 변수를 사용하여야한다. 변수의 범위인 스코프는 좁을 수록 좋다. 전역 변수는 지역 변수보다 탐색시간이 더 길다.

  • 전역 변수 사용을 최소화하는 방법은 전역 변수 객체를 만들어서 사용하는 것이다. - 더글라스 크락포드
  • 그런데 이제 let, const가 나와서 이런 걱정은 하지 않아도 될 듯 하다.

즉시실행함수를 이용한 전역 변수 사용 억제

전역변수 사용을 억제하기 위해 즉시실행함수(IIFE)를 사용할 수 있다. 즉시 샐행 함수는 즉시 실행되고 그 후 전역에서 바로 사라진다.

Screen Shot 2019-12-01 at 1 48 57 PM

정리

자바스크립트는 함수 레벨 스코프이다.

  • 함수 외부에서 생성한 변수는 모두 전역 변수이다.
  • for문에서 선언한 변수는 코드 블록 외부에서 참조할 수 있다.
  • var 키워드 생략이 허용된다, 즉 전역에서 암묵적으로 선언해버린다.
  • 변수 중복 선언이 허용된다.
  • 변수 호이스팅이 된다. 변수를 선언하기 이전에 참조할 수 있다.

그래서 ES6에서는 let, const 키워드를 도입하였다.

let

  1. 블록 레벨 스코프

함수 레벨 스코프란, 함수 내에서 선언된 변수는 함수 내에서만 유효하고, 외부에서는 참조할 수 없는 것을 말한다. 즉, 함수 외부에서 선언한 변수는 모두 전역 변수이다.

블록 레벨 스코프란, 블록 내에서 선언된 변수는 코드 블록 내에서만 유효하다. 코드 블록 외부에서는 참조할 수 없다. 코드 블록 내부에서 선언한 변수는 지역 변수이다. 대부분의 프로그래밍 언어는 블록 레벨 스코프를 따른다.

// 함수 레벨 스코프
var foo = 123; // 전역 변수
console.log(foo); // 123
{
  var foo = 456; // 전역 변수
}
console.log(foo); // 456

ES6에서는 블록 레벨 스코프를 따르는 let 키워드를 도입하였다.

// 블록 레벨 스코프
let foo = 123; // 전역 변수
{
  let foo = 456; // 지역 변수
  let bar = 456; // 지역 변수
}
console.log(foo); // 123
console.log(bar); // ReferenceError: bar is not defined

let 키워드로 선언된 변수는 블록 레벨 스코프를 따른다. 전역에서 선언된 변수명과 블록 내에서 선언된 변수명이 같아도 서로 다른 별개의 변수이다. 위 예제에서 블록 내에서만 선언된 변수 bar는 전역에서 참조할 수 없다.

  1. 변수 중복 선언 금지

var 키워드로는 변수를 중복해서 선언할 수 있지만, let 키워드로는 불가능하다. 중복 선언하면 문법 에러가 발생한다.

Screen Shot 2019-12-01 at 1 58 56 PM

  1. 호이스팅

자바스크립트는 let과 const를 포함하여 모든 선언을 호이스팅한다. 호이스팅이란, 해당 스코프의 선두로 옮긴 것처럼 동작하는 특성을 말한다.

var와는 달리 let 키워드로 선언된 변수는 호이스팅은 되지만, 선언문 이전에 참조할 경우 참조 에러가 발생한다. 이를 일시적 사각지대(Temporal Dead Zone, TDZ)라고 한다. let 키워드로 선언된 변수는 스코프 시작부터 변수 선언이 되기까지 참조를 하지 못하는 것을 말한다.

변수 생성 3단계 : 선언 - 초기화 - 할당

  • 선언 단계 : 변수를 실행 컨텍스트의 변수 객체(VO)에 등록한다. VO는 스코프가 참조하는 대상이 된다.
  • 초기화 단계 : VO에 등록된 변수를 위한 공간을 메모리에 확보한다. 변수는 undefined로 초기화된다.
  • 할당 단계 : 실제 값을 할당한다.

var 키워드로 선언된 변수는 선언과 초기화가 한번에 이루어진다. 그래서 변수 선언문 이전에 변수를 접근하여도 스코프에 변수가 존재하여 에러가 발생하지 않고 undefined를 반환한다. 이를 호이스팅이라고 한다.

Screen Shot 2019-12-01 at 2 10 19 PM

let 키워드로 선언된 변수는 선언과 초기화가 분리되어 진행된다. 스코프에 변수를 등록(선언)하지만, 초기화는 변수 선언문에 도달했을 때 이루어진다. 초기화 이전에 변수에 접근하면 참조 에러가 발생한다. 변수를 위한 메모리 공간이 없으므로, 스코프 시작 지점부터 변수 선언문까지는 해당 변수에 접근할 수 없다. 이 구역을 TDZ라고 한다.

Screen Shot 2019-12-01 at 2 10 29 PM

ES6에서도 호이스팅은 발생한다. 다만 해당 스코프에서 호이스팅된 후 초기화가 이루어지는 시점까지 TDZ에 빠져있기 때문에 참조 에러가 발생한다…. 몇번째 같은 말 반복인데 왜 매번 새롭지

  1. let 키워드로 선언된 변수를 전역 변수로 사용할 때, let 전역 변수는 전역 객체의 프로퍼티가 아니다. let 전역 변수는 보이지 않는 블록 내에 존재하게 된다.
let foo = 123; // 전역변수
console.log(window.foo); // undefined

const

const는 변하지 않는 값을 위해 사용한다.

  1. 선언과 초기화

let은 재할당이 자유롭지만, const는 재할당이 금지된다. const는 선언과 동시에 할당이 이루어져야 한다. const 또한 블록 레벨 스코프를 가진다.

  1. const와 객체

const는 재할당이 금지된다. const 변수의 타입이 객체인 경우, 객체에 대한 참조를 변경하지 못한다는 뜻과 같다. 그래서 이 때 객체의 프로퍼티는 보호되지 않는다. 즉, 할당된 객체의 내용은 변경할 수 있다.

객체 내용이 변경되더라도 객체에게 할당된 주소값은 변경되지 않기 때문에.. 객체 타입 변수 선언에는 const를 사용하는 것이 추천된다.