📚 이전 글 자바스크립트 호출 스택(Call Stack)의 동작 원리-실행 컨텍스트에서 이어집니다.
중첩 함수가 이미 생명주기를 마감한 외부 함수의 변수에 여전히 접근할 수 있을 때, 이때 그 중첩 함수를 클로저 함수라고 한다.
const global = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
const rec = outer();
rec(); //10
위 예시에서 outer 함수는 변수 rec 에 할당되는 순간 inner 함수를 리턴하며 실행이 종료된다.
따라서 outer 내부 변수 const x = 10 또한 효력이 없어진다.
하지만 콘솔 결과값을 보면 10이다. 이것은 inner 함수가 실행될 때 이미 종료된 상태였던 outer의 변수 x를 참조했다는 뜻이다. 어떻게 가능할까?

const rec = outer() 에서 outer 함수가 호출되며 함수 실행 컨텍스트가 생성된다.outer 의 함수 렉시컬 환경도 생성된다.
const rec = outer() 에 의해 outer가 실행되었으나, 변수에 할당되는 순간 inner 함수를 리턴하고 종료된다고 했다. 따라서 실행 컨텍스트에서 pop 되어 사라진다.outer의 함수 렉시컬 환경은 사라지지 않는다.(그림)정적 스코핑 의 특성으로 인해, 자바스크립트의 함수는 태어나면서(선언되는 시점에) 자신의 상위 스코프를 결정하여 참조할 수 있게 된다고 했다.
inner 함수는 태어나면서 자신의 외부 렉시컬 환경인 outer 의 렉시컬 환경을 참조하게 된다. outer의 실행 컨텍스트는 사라졌지만, 그의 렉시컬 환경은 여전히 존재하기 때문에 참조가 가능하다.
➡️ 그래서 inner 함수 내부에서 console.log(x) 가 실행될 때, 변수 x의 참조값을 outer 의 내부에서 찾아 출력할 수 있는 것이다.
그리고 이때의 outer 함수 내 변수 x 와 같은 변수를 자유변수라 한다.
💡 이렇게 중첩 함수가 이미 생명주기를 마감한 외부 함수의 변수에 여전히 접근할 수 있을 때, 이때 그 중첩 함수를 클로저 함수라고 한다.
const makeCalculator = (function () {
let number = 0;
return function (operator, n) {
number = operator(number, n);
return number;
};
})();
function increase(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
function decrease(a, b) {
return a - b;
}
function divide(a, b) {
return a / b;
}
console.log(makeCalculator(increase, 2)); // 2
console.log(makeCalculator(multiply, 6)); // 12
console.log(makeCalculator(divide, 4)); // 3
console.log(makeCalculator(decrease, 7)); // -4
✅ 즉시 실행 함수
한 번의 실행 이후 없어지는 함수let isAdult; (function init(age) { let currentAge = age; if (age >= 20) { isAdult = true; } else { isAdult = false; } })(20); console.log(isAdult); // true console.log(currentAge); // Uncaught ReferenceError: currentAge is not defined
init함수는 선언과 동시에 실행되는 즉시 실행 함수이다.
- 실행될 때 매개변수 20을 통해
currentAge는 20으로 업데이트된다.- 그리고
age가 20 이상이기 때문에, 조건문에 따라 전역변수isAdult가 true로 할당된다.- 그리고
init의 실행이 종료되며, 함수의 지역변수인currentAge도 사라진다.
⭐️ 따라서 콘솔에currentAge를 출력하려 하면 ReferenceError를 보게 된다. 이미currentAge는 사라졌기 때문이다.
makeCalculator 함수는 즉시실행함수로서, 코드가 실행되자마자 함수 내부가 실행되며 사라진다. 하지만 사라지는 동시에 이런 객체를 반환한다. 편의상 리턴객체 라고 부르겠다.function (operator, n) {
number = operator(number, n);
return number;
};
makeCalculator(increase, 2) 은 위의 리턴객체에 각각increase와 2를 대입한 것과 같아진다.
하지만 리턴객체 내부를 실행하려고 보니 number 이라는 변수가 필요하다.
이 리턴객체는 익명 함수의 클로저이기 때문에, 사라져버린 익명 함수 내부의 변수에 접근이 가능하다.
따라서 첫번째 콘솔로그에서
function (increase, 2) {
number = increase(0, 2);
return number; //2
};
해당 동작이 실행되어 number 값이 2로 업데이트된다.
makeCalculator 를 함수 표현식으로 바꾸면 결과가 똑같을까?const makeCalculator = () => {
let number = 0;
return function (operator, n) {
number = operator(number, n);
return number;
};
};
function increase(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
function decrease(a, b) {
return a - b;
}
function divide(a, b) {
return a / b;
}
console.log(makeCalculator()(increase, 2)); // 2
console.log(makeCalculator()(multiply, 6)); // 0
console.log(makeCalculator()(divide, 4)); // 0
console.log(makeCalculator()(decrease, 7)); // -7
makeCalculator 함수 내부에는 let number = 0 변수가 존재한다. 함수가 콘솔로그마다 새로이 실행된다는 건, 이 let number = 0도 새로이 실행된다는 뜻이다.const counter = () => {
let value = 0;
return {
add: (n) => (value += n),
getValue: () => {
console.log(value);
},
};
};
let c = counter();
c.add(5);
c.add(9);
c.getValue(); // 14
counter 함수는 변수 c에 할당될 때 add 와 getValue 라는 두 가지 함수 객체를 리턴하면서 실행이 종료된다.
add 를 두 번 호출했는데 그 두 번 각각의 매개변수끼리 더해진 값이 getValue 를 통해 얻어지므로, 뭔가 counter 내부에 값을 저장해놓을 만한 것이 필요할 것이다.
➡️ 그것이 let value = 0;
초기 value 값을 0으로 해놓은 뒤, 첫 번째 add를 호출하고 매개변수에 5를 대입한다. 이 5를 저장시켜야하므로, value 값에 5를 더해 값을 업데이트한다.
⭐️ add: (n) => (value += n) 가 counter의 클로저 함수이기에 let value = 0; 를 참조할 수 있는 것이다
add를 한 번 더 호출하고 이번엔 매개변수에 9를 담는다. 좀전에 value 값을 5로 업데이트 해놨고, 이번에도 add 함수를 통해 value 값을 14로 업데이트한다.
getValue() 는 최종적인 value 값을 콘솔에 찍어낸다.
const multiply=(x1, x2)=> {
if (x2) {
return x1 * x2
}
return (n) => {
return x1 * n
}
}
multiply(2, 4); // 8
multiply(3, 5); // 15
const double = multiply(2);
double(2); // 4
double(8); // 16
const hexa = multiply(6);
hexa(6); // 36
hexa(10); // 60
multiply 라는 곱셈 함수가 존재하고, 매개변수가 두 개일 때와 한 개일 때가 나뉜다. 따라서 내부에 if문으로 케이스를 나누고, 두 번째 매개변수가 존재한다면 두 변수의 곱을 반환한다.
하지만 두 번째 매개변수가 없다면 클로저 함수를 반환한다.
double 이라는 변수에 리턴 함수를 할당하며 multiply 함수의 실행은 종료된다.
⭐️ 이때 multiply에 2라는 매개변수가 할당된 것을 보자. 클로저 함수는 상위 함수 스코프의 변수 뿐만 아니라 매개변수 또한 참조할 수 있다.
따라서 return (n) => { return x1 * n } 이 클로저 함수의 x1은 multiply의 매개변수 2가 된다.
double(2) 는 2 * 2인 4가 된다.
// 보조 함수
function increase(n) {
return ++n;
}
// 보조 함수
function decrease(n) {
return --n;
}
// 함수로 함수를 생성한다.
// makeCounter 함수는 보조 함수를 인수로 전달받아 함수를 반환한다.
function makeCounter(f) {
let initial = 0;
return () => {
initial = f(initial);
return initial;
};
}
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2
// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동하지 않는다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2
makeCounter 함수에 보조 함수 increase와 decrease를 매개변수로 넣을 수 있다.
increaser 를 먼저 보자. makeCounter는 increaser에 할당되며 함수를 리턴하고 종료된다. 이때 increaser 를 실행했을 때 어떠한 값이 업데이트되어 저장되어야 한다.
클로저가 참조할 수 있는 변수 선언 라인 let initial = 0 으로 초깃값을 설정한다.
그리고 리턴 함수( = 클로저 함수) 내부에서 increase(n) 함수의 사용을 통해 initial 값을 업데이트 한 후에 리턴한다.
따라서 첫 번째 console.log(increaser()) 에서는 1로 업데이트된 initial 값이 출력이 되며, 두 번째 console.log(increaser()) 에서는 이미 initial 값이 1로 변해있는 상태에서 실행되기 때문에 2로 업데이트되어 출력된다.
decreaser 는 increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동하지 않는다.
출처
클로저
React Hooks - vanilla JavaScript로 구현하기
실행 컨텍스트
[10분 테코톡] 🍧 엘라의 Scope & Closure
[JS]클로져(closure)와 클로져의 사용 예제
JavaScript - 즉시실행함수(IIFE)