ES6 - Lexical Environment

Lexical Environment

자바스크립트에서 실행 중인 함수, 코드 블록, 스크립트는 렉시컬 환경이라는 internal hidden associated object를 갖는다. 이 객체는 환경 레코드, 외부 렉시컬 환경으로 구성된다.

환경 레코드는 모든 지역 변수를 프로퍼티로 저장하고 있는 객체로, this 값과 같은 기타 정보도 포함된다. 외부 렉시컬 환경은 외부 코드와 연관되어 있는 객체이다(외부를 참조하는 객체). 다시 말하면 렉시컬 환경은 변수가 저장되는 환경 레코드가 있고, 외부를 참조할 수 있다.

스크립트의 전역과 관련된 렉시컬 환경은 전역 렉시컬 환경이라고 하는데, 전역 렉시컬 환경은 외부 참조를 가질 수 없기 때문에 외부 참조 객체가 nulls 이다.

image

함수 선언

함수는 변수와 마찬가지로 값이다. 하지만 함수를 선언하면 일반 변수와는 달리 곧바로 초기화가 된다. 그래서 함수 선언문으로 선언한 함수는 렉시컬 환경이 만들어지는 즉시(코드가 실행되는 즉시) 사용할 수 있다. 변수를 let을 만나 선언이 될 때까지 사용할 수 없다.

함수 표현식의 경우 함수를 변수로 할당하는 것이기 때문에 해당하지 않는다.

내부,외부 렉시컬 환경

코드가 실행되는 순간 전역 렉시컬 환경이 만들어지는데, 그 안에 함수가 호출되는 경우에는 호출 중인 함수를 위한 내부 렉시컬 환경이 또 만들어진다. 그리고 내부 렉시컬 환경은 외부 렉시컬 환경에 대한 외부 참조를 가질 수 있다.

함수를 반환

함수가 호출되면, 호출될 때마다 새로운 렉시컬 환경이 만들어진다. 그리고 내부 렉시컬 환경에서 함수를 리턴하는 방식의 경우, 이를 중첩 함수라고 한다. 여기서 모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억한다. 함수는 [[Environment]]라는 숨김 프로퍼티를 가진다. 여기서 함수가 만들어진 곳의 렉시컬 환경에 대한 참조(외부 환경 참조 객체)가 저장된다.

그래서 중첩 함수로 만들어진 함수라도 생성된 랙시컬 환경을 알 수 있는 것이다.

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();
alert(counter());

위 코드를 실행하면 전역 렉시컬 환경 -> makeCounter 렉시컬 환경 -> makeCounter가 리턴하는 함수의 렉시컬 환경 순으로 새로운 렉시컬 환경을 생성해낸다. 그리고 makeCounter에는 변수값 또한 저장된다.

counter() 호출하면 각 호출마다 새로운 렉시컬 환경이 만들어진다. 그리고 이 렉시컬 환경은 counter.[[Environment]]에 저장된 렉시컬 환경을 외부 환경으로서 참조할 수 있게 된다.

그래서 counter.[[Environment]]에는 {count: 0}이 있는 렉시컬 환경에 대한 참조가 저장된다. [[Environment]]는 함수가 생성되는 시점 딱 한 번 그 값이 세팅된다. 그리고 영원히 변하지 않는다.

그렇게 counter() 함수를 호출하면, count 변수를 1 증가시키기 위해 count 변수가 필요해지는데, 먼저 자신의 렉시컬 환경에서 변수를 찾는다. 없으면 [[Environment]]에서 참조하는 외부 렉시컬 환경에서 count를 찾는다. 그렇게 찾으면 해당 값을 1 증가시킨다. 변수값을 갱신시키는 곳은 변수가 저장된 렉시컬 환경에서 이루어진다.

클로저라는 개념이 여기에서 나온다. 클로저는 외부 변수를 기억하고 접근할 수 있는 함수를 의미한다. 자바스크립트는 모든 함수가 클로저가 될 수 있다. (예외는 new Function 문법에서…) 이 이유가 [[Environment]] 때문이다.

가바지 컬렉션

함수 호출이 끝나면, 함수에 대응하는 렉시컬 환경이 메모리에서 제거된다. 그래서 함수에 관련된 변수들은 이 때 모두 사라진다. 그래서 자바스크립트에서 객체는 도달 가능한 상태일 때만 메모리에 유지된다.

그런데 호출이 끝난 후에도 도달 가능한 중첩 함수가 있을 수 있다. 이 중첩 함수는 [[Environment]] 프로퍼티에 외부 함수 렉시컬 환경에 대한 정보가 저장된다. 그래서 도달 가능한 상태가 된다.

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // g.[[Environment]]에 f() 호출 시 만들어지는
// 렉시컬 환경 정보가 저장됩니다.

이런식으로 중첩 함수를 사용하는 경우, 호출 시 만들어지는 렉시컬 환경 모두가 메모리에 유지되기 때문에(호출할 때마다 렉시컬 환경이 모두 만들어진다.), 중첩 함수를 아래와 같이 도달할 수 없는 상태로 만들어야 메모리에서 지워진다.

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // g가 살아있는 동안엔 연관 렉시컬 환경도 메모리에 살아있습니다.

g = null; // 도달할 수 없는 상태가 되었으므로 메모리에서 삭제됩니다.

하지만 사실 자바스크립트 엔진이 최적화를 한다. 자바스크립트 엔진은 변수 사용을 분석, 외부 변수가 사용하지 않는다고 판단하면 메모리에서 삭제한다.

function f() {
  let value = Math.random();

  function g() {
    debugger; // Uncaught ReferenceError: value is not defined가 출력됩니다.
  }

  return g;
}

let g = f();
g();

// 예시
let value = "이름이 같은 다른 변수";

function f() {
  let value = "가장 가까운 변수";

  function g() {
    debugger; // 콘솔에 alert(value);를 입력하면 '이름이 같은 다른 변수'가 출력됩니다.
  }

  return g;
}

let g = f();
g();

위의 예시는 최적화 대상이 되어 value 값이 사라진 경우이다. V8 엔진에서만의 이런 경우를 낳는다.

Reference