클로저는 실행 컨텍스트를 알고 모르고에 따라 이해도가 확 달라지니 실행 컨텍스트를 먼저 공부하도록 하자.
외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이런 함수를 클로저라고 부른다.
-자바스크립트 딥 다이브-
function outer() { // 외부 함수
var a = 2;
var b = 2;
return function inner() { // 중첩 함수
console.log(a);
}
}
var func = outer();
func(); // 2
위 코드를 분석하면 먼저 outer 함수가 평가된다. outer 함수가 평가되며 inner 함수도 평가된다.
함수 객체를 생성하는 시점은 함수가 정의된 환경, 즉 상위 함수가 평가 또는 실행되고 있는 시점이다.
- 함수 표현식이라면 변수 호이스팅이 발생하기 때문에 상위 함수가 실행되는 시점에 평가된다.
평가되며 생성된 함수 객체 내부에는 [[Environment]]
라는 내부 슬롯이 존재한다.
이 [[Environment]] 내부 슬롯에는 상위 스코프를 저장
한다. 이때 저장된 상위 스코프는 현재 실행 중인 실행 컨텍스트의 렉시컬 환경이다.
(이 부분을 모르겠다면 스코프를 먼저 공부해보자)
- outer 함수는 전역 함수니까 window의 렉시컬 환경이 저장
- inner 함수는 outer 함수의 렉시컬 환경이 저장
var func = outer()
밑에서 두 번째 줄에서 outer 함수가 실행되었다. 함수가 실행되면 실행 컨텍스트와 렉시컬 환경이 생성된다.
그리고 평가 단계에서 저장해둔 [[Environment]] 내부 슬롯에 저장된 값을 생성된 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 할당
한다. (스코프에서 outer 값이라고 했던 부분, 함수 이름이랑 헷갈릴까 봐 책에서 쓰는 명칭 그대로 적었다.)
이제 함수가 끝까지 실행되고 inner 함수가 반환되며 outer 함수의 생명 주기가 끝나게 된다.
func()
이어서 outer 함수가 반환한 inner 함수가 실행된다. 그럼 outer 함수와 동일하게 상위 스코프가 할당되는 과정을 거친다.
그런데 inner 함수는 outer 함수의 지역 변수인 a
를 참조하고 있다. outer 함수의 생명주기는 이미 끝났는데 이게 어떻게 동작할 수 있을까?
함수가 실행되면 실행 컨텍스트와 렉시컬 환경이 생성된다. 그리고 함수가 종료되면 함께 사라질 것 같지만 그렇지 않다. 자바스크립트에는 가비지 컬렉터가 있다. 나도 자세히는 모르겠지만..
참조-세기(Reference-counting) 가비지 콜렉션
참조-세기 알고리즘은 가장 소박한 알고리즘입니다. 이 알고리즘은 "더 이상 필요 없는 오브젝트"를 "어떤 다른 오브젝트도 참조하지 않는 오브젝트"라고 정의합니다. 이 오브젝트를 "가비지"라 부르며, 이를 참조하는 다른 오브젝트가 하나도 없는 경우, 수집이 가능합니다.
mdn의 설명을 보면 자바스크립트의 가비지 컬렉터는 아무 곳에서도 참조되지 않는 값들을 가비지로 판단하고 메모리를 해제한다.
위 예제에서 선언한 inner 함수는 상위 스코프로 outer 함수의 렉시컬 환경을 참조
하고 있다.
outer 함수의 실행 컨텍스트는 소멸되었지만, inner 함수에 의해 참조되고 있는 렉시컬 환경은 가비지 컬렉터의 대상이 아니기 때문에 소멸하지 않는 것이다.
덕분에 inner 함수는 outer 함수의 지역 변수를 사용할 수 있게 되었다.
이렇게 클로저에 의해서 참조되는 변수를 자유 변수
라고 한다. 위 예제에서 클로저인 inner 함수가 참조하고 있는 a
는 자유 변수이다. 반대로 b
는 inner 함수가 사용하지 않고 있어 자유 변수가 아니다.
같은 렉시컬 환경에 있었어도 클로저에 의해 참조되지 않는 변수는 브라우저 최적화에 의해 사라진다.
클로저는 외부 함수보다 더 오래 살아남은 중첩 함수를 의미한다. 하지만 오래 살아남아도 참조하고 있는 자유 변수가 없다면 클로저라 할 수 없다.
클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.
const Counter = (function () {
let num = 0;
function Counter() {}
Counter.prototype.increase = function () {
return ++num;
}
Counter.prototype.decrease = function () {
return num > 0 ? --num : 0;
}
return Counter;
}());
클로저를 실행해 보자
const counter = new Counter();
console.log(counter.increase);//1
console.log(counter.decrease);//0
자유 변수 num은 클로저가 아니면 접근할 수 없다.
increase, decrease 메서드가 아니면 값을 변경할 수 없다는 것이다.
이렇게 클로저를 사용하면 외부에서 접근할 수 없기 때문에 상태를 안전하게 변경할 수 있게 된다.
클래스를 사용하지 않고 상태를 관리해야 하는데, 클로저를 사용하지 않았다면 아마 전역 변수를 사용해야 값을 변경할 수 있었을 것이다.(끔찍) 이런 특징 때문에 함수형 코딩에서 유용하게 쓰인다고 한다.
- 상태 관리
- 캡슐화와 정보의 은닉
- 전역 변수 사용 억제