[Javascript] 실행 컨텍스트 관점에서의 클로저
by Jewoo.Song
1. 클로저
이미 생명주기가 끝난 외부 함수의 변수를 참조하는 함수를 클로저라 한다.
MDN 클로저는 함수와 그 함수가 선었되었을 때의 Lexical Environment의 조합이다.
- 실제 개발 도구창에 closure는 외부 함수들의 변수들을 담고 있다.
- 현재 context의 변수는 local에 위치에 있다.
- 개발 도구창에서의 클로저는 생명주기가 끝난 외부 함수의 Lexical Environment이다.
- 클로저로 참조된 외부 변수를 자유 변수(Free Variable)이라 하고 클로저라는 이름은 함수가 자유 변수에 대해 닫혀있다는 의미이다.(
자유 변수를 간직한 함수
)
function outerFunc() {
var x = 1;
return function inner() {
console.log(x * 2);
};
}
var inner = outerFunc();
- 위 예제에서 외부 함수가 호출되고 외부 함수가 반환한 내부 함수(외부 함수의 변수를 참조하는)가 클로저이다.
- 즉, 클로저는 반환된 내부 함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수이다.
2. 실행 컨텍스트 관점에서의 클로저
- outerFunction이 실행될 때 outerFunction의 실행 컨텍스트는 아래와 같이 생성된다.
- 이때 inner는 함수 선언이므로 함수명이 Environment Record의 프로퍼티로 추가되고, 생성된 함수 객체가 값으로 설정된다.
//outer EC
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
x : 1,
inner : <Function Object>
}
outer: <Global Environment>,
this: <Global Object>
}
}
- 생성된 함수 객체는 [[scope]] 프로퍼티를 가지고 있다.
- [[scope]] 프로퍼티는 outer EC의 LexicalEnvironment를 가리킨다.
inner = {
[[scope]] : <outer EC's LexicalEnvironment>
}
inner();
- 반환된 내부 함수가 호출되면 내부 함수의 실행 컨텍스트가 실행 컨텍스트 스택에 쌓이고,
- Lexical Environment의 Environment Record가 초기화되고, Outer Reference와 this가 결정된다.
- 이때 Outer Reference는 inner 함수의 [[scope]] 프로퍼티가 가리키는 대상을 가리킨다.
- 즉, outer 함수의 LexicalEnvironment를 가리킨다.
- 이것이 렉시컬 스코프의 실체이다.
- 내부 함수에서 x(외부 함수의)에 접근할 수 있는 것은 렉시컬 스코프의 레퍼런스를 차례대로 저장하고 있는 실행 컨텍스트의 스코프 체인을 검색해서이다.(
outer Reference
)
이처럼 외부 함수보다 내부 함수가 더 오래 유지되는 경우, 외부 함수 바깥에서 내부 함수가 호출되더라도 외부 함수의 지역 변수를 접근할 수 있다. 이러한 함수를 클로저라 한다.
3. 클로저의 활용
1) 상태 유지(전역 변수를 사용하지 않는)
var toggle = (function () {
var isShow = false;
return function () {
// 클로저 반환
box.style.display = isShow ? "block" : "none";
isShow = !isShow; // 상태 변경
};
})();
- 클로저를 제거하지 않는 한 클로저가 기억하는 렉시컬 환경의 변수 isShow는 유지된다.
- 즉, 현재 상태를 저장한다.
- 이처럼 현재 상태를 유지하고, 상태가 변경되었을 때 이 최신 상태를 유지해야 하는 상황에서 클로저는 매우 유용하다.
2) 캡슐화(정보 은닉)
function Counter() {
var counter = 0;
this.increase = function () {
// 클로저
return ++counter;
};
this.decrease = function () {
// 클로저
return --counter;
};
}
var counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
- 생성자 함수 Counter는 increase, decrease 메소드를 갖는 인스턴스를 생성한다.
- 이 메서드들은 자신이 생성됐을 때의 렉시컬 환경인 Counter 생성자 함수의 스코프에 속한 counter 변수를 기억하는 클로저이고 렉시컬 환경을 공유한다.
- counter 변수는 this에 바인딩 된 프로퍼티가 아닌 변수다. 즉, 외부에서 접근할 수 없다.
- 생성자 함수가 생성한 객체는 객체의 프로퍼티뿐 아니라, 자신이 기억하는 렉시컬 환경의 변수도 접근 가능하다.
- 이러한 클로저의 특성을 이용해서 클래스 기반 언어의 private 키워드를 흉내 낼 수 있다.
4. 클로저 사용 시 주의할 점
- 루프 안에서의 클로저 사용
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = function () {
return i;
};
}
for (var j = 0; j < arr.length; j++) {
console.log(arr[j]());
}
// 5
// 5
// 5
// 5
// 5
- for 문에서 사용하는 i는 전역 변수이기 때문에 호출될 때의 전역 변수 i는 5이므로 5가 5번 찍히게 된다.
- 클로저를 활용해서 해결할 수 있다.
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = (function (id) {
//id에 i를 할당하고 내부 함수를 반환한다.
return function () {
return id; // id는 상위 스코프의 자유 변수이므로 값이 유지된다.
};
})(i);
}
for (var j = 0; j < arr.length; j++) {
console.log(arr[j]());
}
let을 이용한 해결
let을 이용해서 더 간단하게 해결할 수 있다.
- let으로 i를 선언하면 i는 for 문의 블록 스코프가 된다.
- 이때 for 문의 반복 횟수만큼 클로저가 생성되고 for 문이 돌 당시의 클로저의 i에 접근하는 것이다.
var arr = [];
for (let i = 0; i < 5; i++) {
arr[i] = function () {
return i;
};
}
for (var j = 0; j < arr.length; j++) {
console.log(arr[j]());
}
// 0
// 1
// 2
// 3
// 4
- 클로저는 자신이 생성될 때의 환경을 기억해야 하므로 메모리 상의 손해를 볼 수 있다.
- 스코프 체인상 뒤쪽에 있는 객체를 참조하므로 성능 저하의 원인이 될 수 있다.
- 클로저의 프로퍼티 값이 쓰기 가능하므로 그 값이 여러 번 호출로 항상 변할 수 있다.
- 하나의 클로저가 여러 함수의 스코프 체인에 들어가 있는 경우도 있다.
참고
- Learning Javascript
- Inside Javascript
- https://poiemaweb.com/js-closure
- https://wit.nts-corp.com/2014/06/20/1560
Subscribe via RSS