MDN 에서 클로저는 아래와 같이 정의한다.
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
const x = 1;
function outerFunc() {
const x = 10;
function innerFunc() {
console.log(x); // 10
}
innerFunc();
}
outerFunc();
const x = 1;
function outerFunc() {
const x = 10;
innerFunc();
}
function innerFunc() {
console.log(x); // 1
}
outerFunc();
자바스크립트는 렉시컬 스코프를 따르는 프로그래밍 언어기 때문에, 두 코드의 실행 결과가 차이가 있는 것이다.
아마 예에전의 포스팅에서도 한번 다뤘었을텐데,
자바스크립트 엔진은 함수를 어디서 호출했는지가 아닌 어디서 정의했는지에 따라 상위 스코프를 결정한다.
이를 렉시컬 스코프(정적 스코프) 라고 칭한다.
함수의 상위 스코프는 함수를 정의한 위치에 의해 정적으로 결정되고, 변하지 않는다.
스코프의 실체는 결국 실행 컨텍스트의 렉시컬 환경이기 때문에,
함수의 상위 스코프를 결정하는 것은 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조값을 결정하는 것과 같은 것이다.
이러한 개념을 반영한다면 아래와 같이 정의내릴 수 있다.
렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값, 즉 상위 스코프에 대한 참조는
함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다.
이것이 바로 렉시컬 스코프다.
함수 객체의 내부 슬롯 [[Environment]]
렉시컬 스코프가 가능하려면 함수는 자신이 정의된, 즉 자신의 상위 스코프를 저장하고 있어야한다.
이는 내부슬롯 [[Environment]] 가 저장하고 있다.
이 내부슬롯에 들어가는 값은 함수 정의가 평가되어 함수 객체를 생성하는 시점에서
현재 실행중인 실행 컨텍스트를 가리키는 참조값이 된다.
함수 객체는 내부 슬롯 [[Environment]] 에 저장한 렉시컬 환경의 참조, 즉 상위 스코프를 자신이 존재하는 한 기억하고 있다.
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x);
}
foo();
bar();
위 코드의 foo 함수와 bar 함수는 모두 전역에서 함수 선언문으로 정의되었다.
함수 객체의 내부 슬롯 [[Environment]] 에는 전역 렉시컬 환경의 참조가 저장된다.
이후 함수가 호출되면 함수 내부로 함수 코드 제어권이 이동하고, 함수 코드가 평가된다.
이때 함수 렉시컬 환경의 구성 요소인 외부 렉시컬 환경에 대한 참조에는
함수 객체의 내부슬롯 [[Environment]]에 저장된 렉시컬 환경의 참조가 할당된다.
클로저와 렉시컬 환경
const x = 1;
function outer() {
const x = 10;
const inner = function () { console.log(x); };
return inner;
}
const innerFunc = outer();
innerFunc(); // 10
(inner 함수는 함수 표현식으로 정의되어 런타임에 평가된다)
outer 함수를 호출하면 outer 함수는 inner 함수를 반환하고 생명주기는 끝나게 된다.
하지만 outer 함수의 변수 x 가 출력됨을 확인할 수 있다.
outer 함수의 실행 컨텍스트는 끝났는데, 내부의 값을 참조 가능하다?
이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가
종료한 외부 함수의 변수를 참조할 수 있다.
이러한 중첩 함수를 클로저라고 하는 것이다.
자바스크립트의 모든 함수는 자신의 상위 스코프를 기억한다.
outer 함수의 실행이 종료하면 inner 함수를 반환하며 outer 함수의 실행 컨텍스트가
실행 컨텍스트 스택에서 제거되지만,
outer 함수의 렉시컬 환경은 소멸되지 않는다.
outer 함수의 렉시컬 환경은 inner 함수의 [[Environment]] 내부 슬롯에 의해 참조되기 때문에
가비지 컬렉션의 대상이 되지 않는다.
위의 예제와는 별개로, 상위 스코프의 어떤 식별자도 참조하지 않는 경우
대부분의 모던 브라우저는 최적화를 통해서 상위 스코프를 기억하지 않는다.
만약 상위 스코프의 식별자들 중 하나의 식별자만 참조한다면,
최적화를 통해 상위 스코프의 식별자 중에서 클로저가 참조하는 식별자만을 기억한다.
클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수라고 부른다.
클로저란 "함수가 자유 변수에 대해 닫혀있다." 라는 의미이다.
쉽게 말하자면 자유 변수에 묶여있는 함수이다.
뭔가,, 이름이 굉장히 직관적이고 낭만(?) 적이다.
클로저 활용
let num = 0;
const increase = function () {
return ++num;
}
위 코드는 카운트 상태가 increase 함수 호출 전 유지가 되어야하고,
increase 함수 만이 num 변수를 제어할 수 있어야한다는 제약조건이 따른다.
이를 클로저를 활용하여 의도치 않은 상태 변경을 방지할 수 있다.
const increase = (function() {
let num = 0;
return function () {
return ++num;
};
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
위 코드가 실행되면 즉시 실행 함수가 호출되고 즉시 실행 함수가 반환한 함수가
increase 변수에 할당된다.
이 increase 변수에 할당된 함수는 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저다.
즉시 실행 함수는 호출 이후 소멸되지만
이 즉시 실행 함수가 반환한 클로저는 increase 변수에 할당되어 호출된다.
즉시 실행 함수는 한번만 실행되므로 num 변수가 재차 초기화될 일은 없을 것이다.
또한 num 변수는 private 변수이므로 의도되지 않은 변경이 일어날 수 없다.
이처럼 클로저는 상태가 의도치않게 변경되지 않도록 은닉하고,
특정 함수에게만 상태 변경을 허용해 상태를 안전하게 변경하고 유지할 수 있다.
위 예제를 좀 더 업그레이드 시켜보자.
const counter = (function () {
let num = 0;
return {
increase() {
return ++num;
},
decrease() {
return num > 0 ? --num : 0;
}
};
}());
console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0
똑같은 예제를 생성자 함수로 표현한 것이다.
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
함수형 프로그래밍에서 클로저를 활용하는 예시다.
function makeCounter(aux) {
let counter = 0;
return function () {
counter = aux(counter);
return counter;
};
}
function increase(n) {
return ++n;
}
function decrease(n) {
return --n;
}
const increaser = makeCounter(increase);
console.log(increaser()); // 1
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
위 코드에서는 makeCounter 함수를 두 번 호출하여
두 개의 makeCounter 함수 컨텍스트가 각각 존재하게 된다.
위와 같은 상황을 방지하려면 아래와 같이 코드를 작성하면 된다.
const counter = (function () {
let counter = 0;
return function (aux) {
// 인수로 받은 보조 함수에 상태 변경 위임
counter = aux(counter);
return counter;
};
}());
function increase(n) {
return ++n;
}
function decrease(n) {
return --n;
}
console.log(counter(increaser)); // 1
console.log(counter(decreaser)); // 0
'Archive > Develop' 카테고리의 다른 글
[ 모던 자바스크립트 스터디 ] 배열 평탄화 + 배열 고차 함수 (0) | 2022.10.21 |
---|---|
[ 모던 자바스크립트 스터디 ] 클래스 (0) | 2022.10.14 |
[ 모던 자바스크립트 스터디 ] 실행 컨텍스트 (1) | 2022.10.12 |
[ 모던 자바스크립트 스터디 ] 프로토타입 - 2 (0) | 2022.10.02 |
[ 모던 자바스크립트 스터디 ] 프로토타입 - 1 (0) | 2022.10.02 |