A closure is the combination of a function and the lexical environment within which that function was declared. (MDN)
클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합이다.
함수가 선언된 렉시컬 환경
함수가 선언된 렉시컬 환경은 outer에 대한 개념, scope chaining과 연관있다.
- 콜스택에 쌓인 실행 컨텍스트 하나 하나는 record와 outer를 담고 있다.
- 특히 outer가 외부의 값을 참조하는 기준이 되는데,
그 outer에 담긴 환경이함수가 선언된 렉시컬 환경(LE)이다.
콜스택에 실행컨텍스트가 쌓이는 것을 생각하며 console.log(x)가 어떤 값을 출력하게 될지 생각해보자.
// (전역 scope) 1. x 선언, x에 값 할당
const x = 1;
// (전역 scope) 2. outerFunc 함수 선언
function outerFunc() {
// 💡(outerFunc scope) 4. 함수 내부에서 x 선언, x에 할당
const x = 10;
// 💡(outerFunc scope) 5. 💡innerFunc 함수 선언💡
function innerFunc() {
// (innerFunc scope) 7. x는 ?
console.log(x);
// 7-1. innerFunc scope 내부에서는 할당되거나 선언된 x가 없음
// 7-2. outer를 참고하자!
// outer는 💡innerFunc 함수가 선언될 당시의 LE💡 -> outerFunc scope 참고하기!
// 즉, innerFunc 함수가 선언될 당시의 외부 변수 정보를 살펴보면 x = 10
// ✅ 따라서 x = 10
}
// (outerFunc scope) 6. innerFunc 함수 실행 > innerFunc 함수 내부로 들어감
innerFunc();
}
// (전역 scope) 3. outerFunc 함수 실행 -> outerFunc 함수 내부로 들어감
outerFunc();
innerFunc() 내부의 console.log(x)에서 참조하고 있는 x 값은
scope chain에 의해 바로 바깥쪽 scope를 찾는다.outer를 찾는다.LexicalEnvironment를 가지고 있다.console.log(x)는 10이 출력된다.//(전역 scope) 1. x 선언, 할당
const x = 1;
// ✅ innerFunc()에서는 outerFunc()는 서로 다른 scope를 가지기 때문에
// ✅ innerFunc()에서는 outerFunc()의 x에 접근할 수 없다!
// ✅ Lexical Scope를 따르는 프로그래밍 언어이기 때문이다.
//(전역 scope) 2. outerFunc 함수 선언
function outerFunc() {
//(outerFunc scope) 5. x 선언, 할당
const x = 10;
//(outerFunc scope) 6. innerFunc 함수 "✅ 호출" -> innerFunc 함수 내부로 들어감
innerFunc();
}
//(💡 전역 scope) 3. 💡 innerFunc 함수 선언 💡
function innerFunc() {
//(innerFunc scope) 7. x는?
console.log(x);
// innerFunc scope 내부에 참고할 x 없음
// outer -> 3.innerFunc 함수 선언 될 당시, 즉 전역 scope에서 참조해야함!
// x = 1
}
//(전역 scope) 4. outerFunc 함수 실행 -> outerFunc 함수 내부로 들어감
outerFunc();
JS엔진은 함수를 어디서 ‘호출했는지’가 아니라 함수를 어디에 ‘정의했는지’에 따라 스코프(상위 스코프)를 결정한다.
다시 말하면, “외부 렉시컬 환경에 대한 참조”에 저장할 참조값, 즉, 스코프에 대한 참조는 함수 정의가 평가되는 시점(함수가 선언될 때)에 함수가 정의된 환경(위치)에 의해 결정된다. 이것이 바로 렉시컬 스코프다.
const x = 1;
//global scope 에서 foo함수 ✅ 선언
function foo() {
const x = 10;
//foo scope 에서 bar함수 📢 호출
bar();
}
//global scope 에서 bar함수 ✅ 선언
function bar() {
console.log(x);
// bar scope 내부에서 변수 값 없으므로
// outer 를 참조해야 한다.
// outer: bar함수 ✅ 선언된 시점은 global scope!
// 따라서 x = 1;
}
foo(); // 1 //foo 호출하면 내부 로직 수행되어 bar()가 호출되므로 x = 1
bar(); // 1 //bar 호출하면 내부 로직 수행되므로 x = 1
정의된 환경(함수가 선언되는 시점의 환경)에 대한 정보를 저장하는 곳 : outer
호출되는 환경과는 상관없이 정의된 환경에 대한 정보를 LexicalEnvironment > outer에 기억
🌟🌟🌟🌟🌟
외부 함수보다중접 함수가 더 오래 유지되는 경우,
✅ 중첩 함수는 이미 생명 주기가💥 종료한 외부 함수의 변수를🔥 여전히참조할 수 있다.
← 이 개념에서 중첩 함수가 바로 클로저이다.
const x = 1;
// 1
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
const innerFunc = outer();
innerFunc();
outer 함수를 호출하면 중첩 함수 inner를 반환(return)한다.
그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스탭에서 팝되어 제거된다(역할을 다 했으니깐)
inner 함수는 런타임에 평가된다.
inner함수가 innerFunc에 전달되었는데, 이는 outer 함수의 렉시컬환경을 (여전히) 참조하고 있다.
즉, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다
function foo() {
const x = 1;
const y = 2;
// ❌ 일반적으로 클로저라고 하지 않는다.
function bar() {
const z = 3;
//❌ 상위 스코프의 식별자를 참조하지 않기 때문
console.log(z);
}
return bar;
}
const bar = foo();
bar();
function foo() {
const x = 1;
// ✅ ❌ bar 함수는 클로저였지만 곧바로 소멸한다.
// 외부로 나가서 따로 호출되는게 아니라, 선언 후 바로
// => 실행 + 소멸
// ❌ 이러한 함수는 일반적으로 클로저라고 하지 않는다.
function bar() {
debugger;
//✅ 상위 스코프의 식별자를 참조한다.
console.log(x);
}
bar();
}
foo();
function foo() {
const x = 1;
const y = 2;
// 클로저의 예
// ✅ 중첩 함수 bar는 외부 함수보다 더 오래 유지되며
// ✅ 상위 스코프의 식별자를 참조한다.
function bar() {
debugger;
console.log(x);
}
return bar;
}
const bar = foo();
bar();
// 카운트 상태 변경 함수 #1
// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터를 구현해요!
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3
// 하지만 이 방법은 변수 num이 은닉되지 않았기 때문에
// num = 100; 중간에 이런식으로 바꿔버릴 수 있어서 위험하다는 💥치명적인 단점이 있다.
보완해야 할 사항
1. 카운트 상태(num 변수의 값)는 increase 함수가 호출되기 전까지 변경되지 않고 유지돼야 한다.
2. 이를 위해 카운트 상태(num 변수의 값)는 increase 함수만이 변경할 수 있어야 한다.
- 누구나 다 접근 가능한 전역변수인 num이 문제 -> 지역변수로 바꿔보자!
// 카운트 상태 변경 함수
const increase = function () {
// num을 지역변수로!
let num = 0;
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
console.log(increase()); //1
num = 100;
console.log(increase()); //1 😵 호출 할 때 마다 num을 초기화 시키니까
console.log(increase()); //1 😵 호출 할 때 마다 num을 초기화 시키니까
보완해야 할 사항
- num 변수는 increase 함수의 지역변수로 선언됐기 때문에 의도치 않는 변경은 방지됐음!
즉, num 변수의 상태는 increase 함수만이 변경할 수 있다.- 하지만, increase()가 호출될 때마다 num이 초기화되어 버린다.
- 의도치 않은 변경은 방지(O)하면서, 이전 상태를 유지해야 함!
클로저를 사용해보자!
const increase = (function () {
let num = 0;
//클로저 함수
return function () {
return ++num;
};
})();
console.log(increase()); // 1
num = 100;
console.log(increase()); // 2
console.log(increase()); // 3
//안전하게 실행된다!
클로저는 함수 정의하고 괄호 열고 닫고 이런 모습 많이 나옴
increase 함수는 아래의 즉시 실행 함수를 실행 한 것
function () {
let num = 0;
//클로저 함수
return function () {
return ++num;
};
}(); // ✅ 괄호 열고 닫았네!?? 즉시 실행 함수로군!
즉 이 부분을 반환 한다.
function () {
return ++num; // num은 외부 환경을 참조한다.
};
따라서 increase 함수를 호출하여 실행하면
console.log(increase());
이 부분을 실행하는 것과 똑같다.
num은 항상 외부 환경의 num을 참조하고 있으므로
가비지 컬렉터는 num을 가져가지 않기 때문에 상태가 유지된다.
function () {
return ++num; // num은 외부 환경을 참조한다.
};
위 코드가 실행되면 즉시 실행 함수가 호출되고 👉 즉시 실행 함수가 반환한 함수가 👉 increase 변수에 할당된다.
increase 변수에 할당된 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저다 => let num = 0;을 기억한다
즉시 실행 함수는 호출된 이후 즉시 소멸되지만, (콜스택에 쌓였다가 바로 사라짐 => 생명주기 끝남)
즉시 실행 함수가 반환한 클로저는 increase 변수에 할당되어 호출된다.
이때 num은 계속 유지되면서 증가
이때 즉시 실행 함수가 반환한 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하고 있다.
따라서 즉시 실행 함수가 반환한 클로저는 카운트 상태를 유지하기 위한 자유 변수 num을 언제 어디서 호출하든지 참조하고 변경할 수 있다.
num은 초기화되지 않을 것이며, 외부에서 직접 접근할 수 없는 은닉된 private 변수이므로, 전역 변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요도 없다.
counter를 변수로 선언해서 increase와 decrease를 객체로 return하도록 하면 될 것 같다.
// 클로저 카운트 기능 확장(값 감소 기능 추가)
const counter = (function () {
//카운트 상태 변수
let num = 0;
// 클로저인 메서드(increase, decrease)를 갖는 객체를 반환한다.
// property는 public -> 은닉되지 않는다. > 그래야 외부에서 프로퍼티 사용 할 수 있으니까
return {
increase() {
return ++num;
},
decrease() {
return num > 0 ? --num : 0;
},
};
})();
console<.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
클로저는 상태(state)가 의도치 않게 변경되지 않도록 안전하게 은닉(information hiding)하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용한다.