어떤 함수A 내부에 선언된 내부함수B 에서 A의 지역변수를 사용하는 경우 발생한다.
var outer = function(){
var a = 1;
var inner = function() {
console.log(++a);
}
inner();
}
outer();
위의 코드의 흐름을 축약하면
- 전역 컨텍스트가 콜스택에 쌓인다
outer
함수가 호출되며outer
실행 컨텍스트가 콜스택에 쌓인다outer
함수가 실행된다.inner
함수가 호출되며inner
실행 컨텍스트가 콜스택에 쌓인다.inner
함수가 실행되며outer
스코프에서 선언된 변수a
를 참조한다.inner
실행 컨텍스트가 콜스택에서 제거된다.outer
실행 컨텍스트가 콜스택에서 제거된다.- 전역 컨텍스트가 콜스택에서 제거되며 종료된다.
이때 inner
에서 a
를 참조하며 inner
의 내부 스코프에 a
를 찾지 못하므로
inner
의 outerEnvironmentReference
를 통해서
외부 스코프인 outer
의 LexicalEnvironment
를 참조하고
그 요소인 environmentRecord
의 식별자 정보를 참조하여 최종적으로 outer의
a
를 찾는다.
하지만 흐름에 따라서 실행 컨텍스트가 순차적으로 제거되고
inner
실행 컨텍스트가 제거되면 outer
실행 컨텍스트의 a
를 참조 카운트가 0이되어
outer
의 실행이 끝나면 자연스럽게 outer
실행 컨텍스트도 제거되고
GC
(Garbege Collector)의 수집 대상이 된다.
그럼 의도적으로 클로저를 발생시켜보자
var outer = function(){
var a = 1;
var inner = function() {
console.log(++a);
}
return inner;
}
var outer2 = outer();
console.log(outer2());
위의 코드는 상단의 코드와 비슷하지만 다르다
outer
함수의 실행 결과로 내부에서 선언된 inner
함수 자체를 return했다.
따라서 outer2
에 inner
함수가 할당되었고, 따라서 outer2
를 실행하게 되면
outer
에서 선언된 함수 내부의 a
를 참조할 가능성이 생긴다.
이때 참조할 가능성이 생겼기 때문에 outer
실행 컨텍스트의 참조 카운트는 0이 아니다.
따라서 outer2
의 실행이 종료되기 전 (inner
함수의 실행 컨텍스트가 콜스택에서 제거되기 전)
까지는 콜스택에 outer
의 실행 컨텍스트가 참조되기 위해 유령처럼 남아있게 된다.
아래는 설명에 대한 간략한 흐름도 이다.
이런 클로저를 정의한 몇몇 문장이 있다.
- 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수
- 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수
- 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수
즉 위의 예시에서는 inner
가 클로저에 해당한다.
클로저를 한 마디로 정의하면
어떤 함수A
에서 선언한 변수 a
를 참조하는 내부함수B
를 외부로 전달할 경우
A
의 실행 컨텍스트가 종료된 이후에도 변수 a
가 사라지지 않는 현상 입니다.
그밖에 return 없이 클로저가 발생하는 경우는
지역변수를 참조하는 내부함수를 외부에 전달하면서 발생하는데
대표적인 예로는 setInterval
, setTimeout
, eventListener
등이 있다.
클로저는 앞서 언급했듯이 소멸했어야 할 변수를 참조하고 있는 상황이기 때문에
그만큼 메모리를 소모한다.
하지만 이건 메모리 누수와는 조금 다르다.
클로저를 의도적으로 사용했다면
말 그대로 개발자의 의도대로 동작했기 때문에 메모리 누수 라고는 할 수 없다.
그저 클로저의 특징일 뿐이다.
그럼 코드의 동작중 클로저의 필요성이 사라질 경우
참조 카운트를 의도적으로 0으로 만드는 방법은 뭘까
간단하다.
클로저의 식별자에 참조형이 아닌 기본형 데이터 (보통null
또는 undefined
) 를
할당하면 된다.
var outer = (function(){
var a = 1;
var inner = function(){
return a++;
};
return inner;
})();
console.log(outer());
console.log(outer());
outer = null;
클로저의 동작이 끝난 후 null
을 할당하여 참조 카운트를 0으로 만든다
따라서 outer
의 참조 카운트가 0이 되므로 GC
의 수거 대상이 된다.
대표적인 접근 권한 제어의 개념으로 public
, private
이 있다.
이를 클로저를 이용해서 구현할 수 있는데
var outer = function(){
var a = 1;
var inner = function(){
return a++;
}
return inner;
};
var outer2 = outer();
console.log(outer2());
위의 코드를 보면 outer2
에 outer
의 리턴값인 inner
함수를 할당했기 때문에
outer
의 지역변수 a
를 외부에서도 읽을 수 있게 되었다. : public