Lexical Environment & Scope & TDZ

kich555·2021년 9월 11일
2

I don't Know JavaScript

목록 보기
6/9

Lexical Environment


참조 : 자바스크립트의 스코프와 클로저 NHN Cloud

우리가 말하는 이 JavaScript의 스코프는 ECMAScript 언어 명세에서 렉시컬 환경(Lexical environment)환경 레코드(Environment Record)라는 개념으로 정의되었다.

정확히 말하면 `렉시컬 환경`의 하위 개념으로 `환경 레코드`가 존제한다. (위 도표 참고)

렉시컬 환경은 JavaScript 코드에서 변수함수 등의 식별자를 정의하는데 사용하는 객체로 생각하면 쉽다. 렉시컬 환경은 식별자와 참조 혹은 값을 기록하는 환경 레코드outer라는 또 다른 렉시컬 환경을 참조하는 포인터로 구성된다. outer는 외부 렉시컬 환경을 참조하는 포인터로, 중첩된 JavaScript 코드에서 스코프 탐색을 하기 위해 사용한다.

예를 한번 들어보자

function wine() {
  var white = 'white'
  var red = 'red'
  var rose = 'rose'   
  function sortWine() { .... }
}
wine()

위의 코드는 아래와 같은 렉시컬 환경을 가진다.

{
  environmentRecord: {
    white: 'white',
    red: 'red',
    rose: 'rose',
    sortWine: <Function>
  }
  outer: global.[[Environment]]
}

변수는 특수한 내부 객체인 환경 레코드프로퍼티일 뿐입니다. 변수를 가져오거나 변경하는 것은 환경 레코드프로퍼티를 가져오거나 변경함을 의미합니다.

이는 간단한 예시일 뿐 다른 많은 요소도 포함한다.

Outer

렉시컬 환경outer에 의해 JavaScript는 클로저(Closure)를 가진다.

JavaScript 코드에서 변수에 접근할 땐, 먼저 내부 렉시컬 환경을 검색 범위로 잡는다. 내부 렉시컬 환경에서 원하는 변수를 찾지 못하면 검색 범위를 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경으로 확장한다. 이 과정은 검색 범위가 전역 렉시컬 환경으로 확장될 때까지 반복된다.
이때 참조를 구분짓는 것이 바로 outer이다.

아래 예시를 보자.

var drink = 'alcohol';

function wineSearcher() {
  var wine = 'wine';
  
  function naturalWine() {
    var orangeWine = 'onrangeWine';
    var petnatWine = 'petnatWine';
    console.log(dirnk); // 'alcohol'
    console.log(wine);     // 'wine'
    console.log(orangeWine);     // 'onrangeWine'
     console.log(petnatWine);     // 'petnatWine'
  }
  naturalWine();
}
wineSearcher();



GlobalEnvironment = {
  environmentRecord: {
    drink: 'alcohol'
  },
  outer: null // 전역 변수 drink는 이미 전역 렉시컬 환경에 존재하기 때문에 outer 로 null값을 가진다.
};

wineSearcher_Environment = {
  environmentRecord: {
    wine: 'wine'
  },
  outer: globalEnvironment // wineSearcher()은 전역 환경내에 선언한 함수이기 때문에 outer로 전역 환경을 참조 할 수 있다.
}

naturalWine_Environment = {
  environmentRecord: {
    onrangeWine: 'onrangeWine'
    petnatWine:'petnatWine'
  },
  outer: wineSearcher_Environment // naturalWine()은 wineSearcher() 내에 선언한 함수이기 때문에 outer로 wineSearcher()를 참조 할 수 있다.
}

Hoisting

호이스팅은 단어 그대로, 식별자가 마치 스코프의 상단으로 끌어올려지는 것 같이 보이는 현상이다.
하지만 굳이 엄격하게 말한다면, 이것은 끌어올려지는 것이 아니다.

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

위 의 foo()를 실행했을때 결과 값은 무엇일까?

foo() // 2

위와같은 상황을 처음보면 혼란스러울 수 있다. a가 선언되지도 않았는데 초기화를 하고 심지어 정상적으로 출력까지 된다.

하지만 우리가 위에서 배웠던 내용들을 정리하자면, 이는 정상적인 동작이다.

  1. JavaScript Engine은 인터프리팅 전에 바이트 코드로 컴파일 과정을 거친다.
  2. JavaScript는 렉시컬 스코프 언어이기 때문에 렉싱 과정 중 모든 식별자의 스코프가 이미 정의된다.
  3. 실행 컨텍스트 시 함수 등 코드를 읽고, 실행하는 것과는 별개로 이미 스코프는 정의되었다.
  4. 이는 식별자를 정의한 부분이 끌어올려지는(호이스팅) 것 처럼 보이지만, 실질적으로는 선언과 실행이 별개의 과정에서 진행된 것 뿐이다.

연습으로 한번더 예시를 살펴보자.

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

위 의 foo()를 실행했을때 결과 값은 무엇일까?

foo() // undefined

undefined로 출력되는 이유는 무엇일까?

var a = 2;

렉싱 과정 중 a는 식별자로써 var로 선언 됨이 정리된다. <- 토큰간의 연관성을 분석하기 때문
그러면 위 식은 이렇게 표현할 수 있을 것이다.

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

즉 함수가 실행되는 과정에서 a가 2로 초기화 되기 전에 console.log(a)는 실행된다.
a는 선언만 되었고 실행되지 않았음으로 undefined를 출력한다.

결국 호이스팅은 스코프 정의 과정 중 즉 소스코드 컴파일레이션 중 렉싱 과정에서 이미 일어나는 일종의 현상이라고 볼 수 있다.

근데 여기서 한가지 의문점이 생긴다.

그렇다면 let이나 const가 호이스팅 되지 않는 이유는 뭘까?

위 시리즈를 잘 이해했다면, var let const는 단순 스코프가 블록레벨을 가질지, 함수 레벨을 가질지 설정 할 뿐이지 어차피 JavaScript는 렉시컬 스코프를 가진다. 즉 어떤 키워드로 변수를 선언하든 호이스팅은 일어난다.는걸 알 수 있다.

TDZ

이번 시리즈의 핵심 왜 var를 사용하면 안되는지에 대해 알아보자.

스코프의 시작 지점부터 초기화 시작 지점까지의 구간을 TDZ(Temporal Dead Zone) 라고한다.

이게 무슨 소리일까?

TDZ에 대해 이해하려면 변수의 선언에 대해 이해가 필요하다.


JavaScript Variables Lifecycle: Why let Is Not Hoisted

javascript에서의 변수는 선언, 초기화, 할당이라는 3가지 단계의 걸쳐서 생성된다.
여기서 var let const는 각각 단계의 순서가 다르게 실행되는데 이에 의해 TDZ가 존재하거나, 존재하지 않을 수 있다.

먼저 var의 라이프 사이클을 살펴보자.

JavaScript Variables Lifecycle: Why let Is Not Hoisted

보다시피 var를 사용하여 변수를 선언하면 선언과 초기화 페이즈가 동시에 일어나고, undefined를 값으로 가진다는걸 볼 수 있다.
TDZ는 스코프의 시작 지점부터 초기화 시작 지점까지의 구간이라 했음으로, 선언과 초기화를 동시에 진행하는 var는 TDZ가 존재하지 않는다.

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

즉 선언과 메모리 할당이 동시에 일어남으로 a는 컴파일 과정에서 이미 undefined라는 값을 갖는다.

다음엔 let의 라이프 사이클에 대해 알아보자

JavaScript Variables Lifecycle: Why let Is Not Hoisted

function foo() {
    console.log(a);
    let a = 2;
}
foo();
>

letvar와 다르게 선언 페이즈와 초기화 페이즈 사이에 초기화 되지않는 시점이 존재한다.
그렇기 때문에 위의 코드에서 호이스팅은 되었지만, 메모리가 할당이 되질 않아 접근할 수 없어 참조 에러(ReferenceError)가 발생하는 것이다.
이것을 호이스팅이 되지 않아 참조 에러를 가지는 것으로 흔히들 오해하는 것이다.

참고로 위의 함수를 실행하면 ReferenceError: Cannot access 'a' before initialization 라는 에러문구를 볼 수 있다. 
a는 초기화 전에 접근할 수 없다

💡결론

var의 호이스팅 때문에 var를 사용하지 않는다고 여러 블로그 및 유튜브, 강의에서 본 뒤로 var호이스팅에 대해 찾아보았지만, 조사 결과 결국 let, const모두 호이스팅이 된다는걸 알아냈다. (하....ㅋㅋTDZ를 뒤늦게 찾아버림)
'var'를 쓰지 않는 이유는 결국 TDZ가 없어서이다. var는 선언과 할당을 동시에 하는 라이프 사이클을 가졌기 때문에 TDZ가 존재하지 않고, 이때문에 많은 개발자들을 혼란스럽게 만든 것 이었다.
(물론 난 그만큼 복잡한 js코드를 짠 적이 없어, 혼란스러운 적이 없었지만...)

💡앞으로 공부할 것 : 위 글 작성을 위해 구글링을 열심히 하던 중 this 바인딩이나 실행 컨텍스트, 클로저 등의 개념이 뿜뿜 나왔다.
(뭘까? 궁금하다.) 클로저는 곧 정리해야하는데..과제하기 바쁘다. 주말에 하자

💡 실행 컨텍스트 파트를 끝내고, 콜 스택과 이벤트루프에 대해서도 정리하고싶다. What the heck is the event loop anyway? (유튜브가 재밌더라)

💡 이벤트 버블링/캡쳐링에 대한 재밌는 유튭영상을 보았다. Learn JavaScript Event Listeners In 18 Minutes 자막없는 영어영상이지만 시간가는줄 모르고 봤다. 개념을 대충 이해하긴 했는데 얘도 정리해야한다.

참조 : JavaScript Variables Lifecycle: Why let Is Not Hoisted
참조 : Don't Use JavaScript Variables Without Knowing Temporal Dead Zone
참조 : 자바스크립트의 스코프와 클로저 NHN Cloud
참조 : 자바스크립트 함수(3) - Lexical Environment NHN Cloud
참조 : execution context의 생성과정 by 방춘덕
참조 : What is JavaScript AST, how to play with it? (stack overflow)
참조 : 변수의 유효범위와 클로저 javascript.info
참조 : https://poiemaweb.com/js-scope
참조 : [JavaScript] 렉시컬 스코프, Lexical Scope / 동적 스코프, Dynamic Scope by CHATI Developer
참조 : 스코프(Scope)란? by Zzolab Blog :)
참조 : You Don't Know JS: Scope & Closures by Kyle Simpson
참조 : You Don't Know JS: Scope & Closures by Kyle Simpson

profile
const isInChallenge = true; const hasStrongWill = true; (() => { while (isInChallenge) { if(hasStrongWill) {return 'Success' } })();

0개의 댓글