클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상이다.
📌 우선 외부함수에서 변수를 선언하고 내부함수에서 해당 변수를 참조하는 형태의 간단한 코드를 보자.
var outer = function() {
var a = 1;
var inner = function() {
return ++a;
};
return inner();
};
var outer2 = outer();
console.log(outer2); // 2
위 코드의 콜스택 및 실행 컨텍스트는 다음과 같다.
👉 outer 함수의 실행 컨텍스트가 종료되면, LexicalEnvironment에 저장된 식별자들(a, inner)에 대한 참조를 지운다 (가비지 컬렉터에 의해 소멸).
👉 함수의 실행 컨텍스트가 종료되기 이전에 inner 함수의 실행 컨텍스트가 종료된다.
👉 따라서 이후에 별도로 inner 함수를 호출할 수 없다.
📌 다음으로, inner 함수의 실행 결과가 아닌 "inner 함수 자체"를 반환하는 코드를 보자.
var outer = function() {
var a = 1;
var inner = function() {
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
** outer2 변수는 outer 함수의 실행 결과인 inner 함수를 참조하게 되므로, outer2()를 호출하면서 inner 함수가 실행된다.
위 코드의 콜스택 및 실행 컨텍스트는 다음과 같다.
👉 inner 함수가 실행될 시점에 outer 함수는 이미 실행이 종료된 상태이다.
👉 하지만 전역 컨텍스트 속 outer2 변수가 inner 함수를 참조하고 있기 때문에 가비지 컬렉터는 a를 수집하지 않는다 (가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값을 수집 대상에 포함시키지 않는다).
👉 따라서 outer 함수의 실행이 종료된 이후에도 inner 함수를 호출할 수 있게 된다.
즉, 클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달한 경우, A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상을 말한다.
// case (1)
for (var i = 1; i < 6; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
🤔 위 코드의 출력은 어떻게 나타날까?
1부터 5까지 1초 간격으로 출력될 것으로 예상했지만,
정답은 6 6 6 6 6 이다.
그 이유는, 자바스크립트 엔진이 for문을 먼저 돌고 나서야 테스크 큐에 쌓인 비동기 콜백함수들을 실행시키기 때문이다.
setTimeout(()=> {
console.log(i);
}, 1 * 1000);
setTimeout(()=> {
console.log(i);
}, 2 * 1000);
setTimeout(()=> {
console.log(i);
}, 3 * 1000);
setTimeout(()=> {
console.log(i);
}, 4 * 1000);
setTimeout(()=> {
console.log(i);
}, 5 * 1000);
즉, 테스크 큐 안에는 다음과 같은 비동기 콜백 함수들이 쌓이는데, 이때 함수 속 i는 콜백함수가 실행될 때 결정된다. i는 이미 for문이 돌고 난 후의 6 값을 가지므로 6이 총 5번 찍히게 된다.
그렇다면 어떻게 의도한 대로 코드를 설계할 수 있을까?
🤩 클로저 관계를 활용하면 된다!
// case (2)
function setTimer(j) {
setTimeout(()=> {
console.log(j);
}, j * 1000);
}
for (var i = 1; i < 6; i++) {
setTimer(i);
}
앞서 언급하였듯, 클로저란 어떤 함수 A에서 선언한 변수 a를 외부로 전달한 경우 A의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않도록 도와주는 것을 의미한다. 따라서 각 i는 해당 함수가 호출될 당시의 i를 기억하게 된다.
function setTimer(j) {
setTimeout(()=> {
console.log(j);
}, j * 1000);
}
setTimer(1);
function setTimer(j) {
setTimeout(()=> {
console.log(j);
}, j * 1000);
}
setTimer(2);
function setTimer(j) {
setTimeout(()=> {
console.log(j);
}, j * 1000);
}
setTimer(3);
function setTimer(j) {
setTimeout(()=> {
console.log(j);
}, j * 1000);
}
setTimer(4);
function setTimer(j) {
setTimeout(()=> {
console.log(j);
}, j * 1000);
}
setTimer(5);
즉, 위와 같은 코드가 실행되며 1 2 3 4 5 가 출력된다.
(출처 : 정재남(2019), [코어 자바스크립트], 위키북스)