Variable scope, closure

김병엽·2024년 7월 29일

자바스크립트는 함수 지향 언어이다.

함수를 동적으로 생성할 수 있고, 생성한 함수를 다른 함수에 인수로 넘길 수 있으며, 생성된 곳이 아닌 곳에서 함수를 호출할 수도 있기 때문이다.


Code blocks

코드 블록 {...} 안에서 선언한 변수는 블록 안에서만 사용할 수 있다.

{
  // 지역 변수를 선언하고 몇 가지 조작을 했지만 그 결과를 밖에서 볼 수 없습니다.

  let message = "안녕하세요."; // 블록 내에서만 변숫값을 얻을 수 있습니다.

  alert(message); // 안녕하세요.
}

alert(message); // ReferenceError: message is not defined

Nested functions

함수 내부에서 선언한 함수는 ‘중첩(nested)’ 함수라고 부른다.

function makeCounter() {
  let count = 0;

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

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2 
}

makeCounter를 살펴보다 보면 “counter를 여러 개 만들었을 때, 이 함수들은 서로 독립적일까? 함수와 중첩 함수 내 count 변수엔 어떤 값이 할당될까?”


Lexical Environment

자바스크립트에선 실행 중인 함수, 코드 블록 {...}, 스크립트 전체는 렉시컬 환경(Lexical Environment) 이라 불리는 내부 숨김 연관 객체(internal hidden associated object)를 갖는다.

렉시컬 환경 객체는 두 부분으로 구성된다.

  • 환경 레코드(Environment Record) – 모든 지역 변수를 프로퍼티로 저장하고 있는 객체. this 값과 같은 기타 정보도 여기에 저장.
  • 외부 렉시컬 환경(Outer Lexical Environment) 에 대한 참조 – 외부 코드와 연관됨.

1. Variables

’변수’는 특수 내부 객체인 환경 레코드의 프로퍼티일 뿐이다.

'변수를 가져오거나 변경’하는 것은 '환경 레코드의 프로퍼티를 가져오거나 변경’함을 의미한다.

이렇게 스크립트 전체와 관련된 렉시컬 환경은 전역 렉시컬 환경(global Lexical Environment) 이라고 한다.

전역 렉시컬 환경은 외부 참조를 갖지 않기 때문에 화살표가 null을 가리키는 걸 확인할 수 있다.


2. Function Declarations

함수는 변수와 마찬가지로 값이다.

함수 선언문으로 선언한 함수는 일반 변수와는 달리 바로 초기화된다는 점에서 차이가 있다.

이런 동작 방식은 함수 선언문으로 정의한 함수에만 적용된다.

let say = function(name)...같이 함수를 변수에 할당한 함수 표현식은 해당하지 않는다.


3. Inner and outer Lexical Environment

함수를 호출해 실행하면 새로운 렉시컬 환경이 자동으로 만들어진다.

이 렉시컬 환경엔 함수 호출 시 넘겨받은 매개변수와 함수의 지역 변수가 저장된다.

함수 호출 중엔 호출 중인 함수를 위한 내부 렉시컬 환경과 내부 렉시컬 환경이 가리키는 외부 렉시컬 환경, 총 2개의 렉시컬 환경을 갖게 된다.

그리고 내부 렉시컬 환경은 외부 렉시컬 환경에 대한 참조를 갖는다.

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


4. Returning a function

function makeCounter() {
  let count = 0;

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

let counter = makeCounter();

중요한 사실이 하나 있다.

모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억한다는 점이다. 함수는 [[Environment]]라 불리는 숨김 프로퍼티를 갖는데, 여기에 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.

따라서 counter.[[Environment]]{count: 0}이 있는 렉시컬 환경에 대한 참조가 저장된다.
호출 장소와 상관없이 함수가 자신이 태어난 곳을 기억할 수 있는 건 바로 이 [[Environment]] 프로퍼티 덕분이다.
[[Environment]]는 함수가 생성될 때 딱 한 번 값이 세팅되고 영원히 변하지 않는다.

counter()를 호출하면 각 호출마다 새로운 렉시컬 환경이 생성된다.
그리고 이 렉시컬 환경은 counter.[[Environment]]에 저장된 렉시컬 환경을 외부 렉시컬 환경으로서 참조한다.

실행 흐름이 중첩 함수의 본문으로 넘어오면 count 변수가 필요한데, 먼저 자체 렉시컬 환경에서 변수를 찾는다.
익명 중첩 함수엔 지역변수가 없기 때문에 이 렉시컬 환경은 비어있는 상황이다(empty).
counter()의 렉시컬 환경이 참조하는 외부 렉시컬 환경에서 count를 찾는다.

찾으면 count++가 실행되면서 count 값이 1 증가해야하는데, 변숫값 갱신은 변수가 저장된 렉시컬 환경에서 이뤄진다.

실행이 종료된 후의 상태는 다음과 같다.

❗️ 클로저(Closure)

클로저는 외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수를 의미합니다.
자바스크립트에선 모든 함수가 자연스럽게 클로저가 됩니다.

자바스크립트의 함수는 숨김 프로퍼티인 [[Environment]]를 이용해 자신이 어디서 만들어졌는지를 기억합니다. 함수 본문에선 [[Environment]]를 사용해 외부 변수에 접근합니다.


Garbage collection

함수 호출이 끝나면 함수에 대응하는 렉시컬 환경이 메모리에서 제거된다.

자바스크립트에서 모든 객체는 도달 가능한 상태일 때만 메모리에 유지된다.

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

함수 호출은 끝났지만 렉시컬 환경이 메모리에 유지되는 이유는 바로 이 때문이다.

function f() {
  let value = 123;

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

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

중첩함수를 사용할 때는 주의할 점이 있다.

f()를 여러 번 호출하고 그 결과를 어딘가에 저장하는 경우, 호출 시 만들어지는 각 렉시컬 환경 모두가 메모리에 유지된다는 점이다.

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

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

// 배열 안의 세 함수는 각각 f()를 호출할 때 생성된
// 렉시컬 환경과 연관 관계를 맺습니다.
let arr = [f(), f(), f()];

예시를 실행하면 3개의 렉시컬 환경이 만들어지는데, 각 렉시컬 환경은 메모리에서 삭제되지 않는다.

렉시컬 환경 객체는 다른 객체와 마찬가지로 도달할 수 없을 때 메모리에서 삭제된다.
해당 렉시컬 환경 객체를 참조하는 중첩 함수가 하나라도 있으면 사라지지 않는다.

function f() {
  let value = 123;

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

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

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

예시 같이 중첩 함수가 메모리에서 삭제되고 난 후에야, 이를 감싸는 렉시컬 환경(그리고 그 안의 변수인 value)도 메모리에서 제거된다.


Reference

javascript.info

profile
선한 영향력을 줄 수 있는 개발자가 되자, 되고싶다.

0개의 댓글