책 코어자바스크립트 (위키북스 출판) 을 참고해 작성한 포스팅입니다.
클로저
클로저란?
“A closure is the combination of a function and the lexical environment within which that function was declared.” - MDN
-> 클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상
선언될 당시의 lexical environment는 실행 컨텍스트의 구성 요소 중 하나인 outerEnvironmentReference에 해당한다.
1
2
3
4
5
6
7
8
var outer = function () {
var a = 1;
var inner = function () {
console.log(a++);
};
inner();
};
outer();
outer 함수에서 변수 a
를 선언했고 outer의 내부함수인 inner 함수에서 a
의 값을 1만큼 증가시킨 후 출력한다. inner 함수 내부에서는 a
를 선언하지 않아 environmentRecord에서 값을 찾지 못하므로 outerEnvironmentReference에 지정된 상위 컨텍스트인 outer의 Lexical Environment에 접근해 a
를 찾는다.
outer 함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들 (a, inner)에 대한 참조를 지운다. 그러면 각 주소에 저장되어 있던 값들은 자신을 참조하는 변수가 없게 되어 가비지 컬렉터의 수집 대상이 된다.
1
2
3
4
5
6
7
8
9
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner();
};
var outer2 = outer();
console.log(outer2);
코드 내용을 조금 바꿔서 다시 보자. inner 함수를 실행한 결과를 리턴하고 있으므로 outer 함수의 실행 컨텍스트가 종료된 시점에는 a 변수를 참조하는 대상이 없어진다. 이 역시 가비지 컬렉터에 의해 소멸된다.
두가지 예시 모두에서 outer 함수의 실행 컨텍스트가 종료되기 이전에 inner 함수의 실행 컨텍스트가 종료됐으며, 이후 별도로 inner 함수를 호출할 수 없다. 그렇다면 outer의 실행 컨텍스트가 종료된 후에도 inner 함수를 호출할 수 있게 하면 어떻게 될까?
1
2
3
4
5
6
7
8
9
10
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
이번에는 inner 함수의 실행 결과가 아닌 inner 함수 자체를 반환했다. outer 함수의 실행 컨텍스트가 종료될 때 outer2 변수는 outer의 실행 결과인 inner 함수를 참조하게 된다.
inner 함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없다. outerEnvironmentReference에는 inner 함수가 선언된 위치의 LexicalEnvironment가 참조복사된다. inner 함수는 outer 함수 내부에서 선언됐으므로 outer 함수의 LexicalEnvironment가 담길 것이다. 이후 스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근해 1만큼 증가시키고 inner 함수의 실행 컨텍스트가 종료된다.
inner 함수의 실행 시점에는 outer 함수는 이미 실행이 종료된 상태인데 outer 함수의 LexicalEnvironment에 접근이 가능한 이유는 무엇일까?
가비지 컬렉터의 동작 방식
가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함하지 않는다. outer 함수는 실행 종료 시점에 inner 함수를 반환하므로 외부함수인 outer 의 실행이 종료되더라도 내부함수인 inner 함수는 언젠가 outer2를 실행함으로써 호출될 가능성이 있다. 언젠가 inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReference가 outer 함수의 LexicalEnvironment를 필요로 하기때문에 가비지 컬렉터의 수집 대상에서 제외된 것이다.
클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상 을 말한다.
클로저와 메모리 관리
클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생한다. 그 필요성이 사라진 시점에 메모리를 더이상 소모하지 않도록 해줌으로 메모리 소모를 막을 수 있다.
1
2
3
4
5
6
7
8
9
10
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
};
console.log(outer());
console.log(outer());
outer = null;
식별자에 참조형이 아닌 기본형 데이터 (null
혹은undefined
)를 할당해 참조 카운트를 0으로 만들어 가비지 컬렉터가 수집할 수 있도록 한다.
클로저 활용 사례
접근 권한 제어(정보 은닉)
자바스크립트는 기본적으로 변수 자체에 접근 권한을 직접 부여하도록 설계되어 있지 않다. 클로저를 이용하면 함수 차원에서 public, private한 값이 구분되게 할 수 있다.
1
2
3
4
5
6
7
8
9
10
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());
outer 함수를 종료할 때 inner 함수를 반환해 outer 함수의 지역변수인 a의 값을 외부에서도 읽을 수 있게 됐다. 이처럼 클로저를 활용하면 외부 스코프에서 함수 내부의 변수들 중 선택적으로 일부의 변수에 대한 접근 권한을 부여할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var createCar = function () {
var fuel = Math.cell(Math.random() * 10 + 10);
var power = Math.cell(Math.random() * 3 + 2);
var moved = 0;
return {
get moved() {
return moved;
},
run: function () {
var km = Math.cell(Math.random() * 6);
var wasteFuel = km / power;
if (fuel < wasteFuel) {
console.log("이동불가");
return;
}
fuel -= wasteFuel;
moved += km;
console.log(
km + "km 이동 (총 " + moved + "km). 남은 연료: " + fuel
);
}
};
};
var car = createCar();
fuel, power 변수는 비공개 멤버로 지정해 외부에서의 접근을 제한했고 moved 변수는 getter만을 부여해 읽기 전용 속성을 부여했다. 외부에서는 오직 run 메서드를 실행하는 것과 현재의 moved 값을 확인하는 것만 가능하다.
부분 적용 함수 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가 나중에 (n-m)개의 인자를 넘기면 원래 함수의 실행 결과를 얻을 수 있는 함수.
1
2
3
4
5
6
7
8
9
var add = function () {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = add.bind(null, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10));
커링 함수 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말한다. 커링은 한 번에 하나의 인자만 전달하는 것을 원칙으로 한다. 또한, 중간 과정상의 함수를 실행한 결과는 그 다음 인자를 받기 위해 대기만 할뿐, 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다.
1
2
3
4
5
6
7
8
9
10
var curry3 = function (func) {
return function (a) {
return function (b) {
return func(a, b);
};
};
};
var getMaxWith10 = curry3(Math.max)(10);
console.log(getMaxWith10(8));
화살표 함수로 표현하면
1
var curry3 = (func) => (a) => (b) => func(a, b);
정리
클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
클로저는 그 본질이 메모리를 계속 차지하는 개념이므로 더는 사용하지 않게 된 클로저에 대해서 메모리를 차지하지 않도록 관리해줄 필요가 있다.