클로저 (closure)
클로저(closure)는 내부 함수와 밀접한 관계를 가지고 있는 주제다.
외부 함수의 실행이 끝나서 외부 함수가 소멸된 이후에도 내부 함수가 외부 함수의 변수에 접근 할 수 있다.
이러한 메커니즘을 클로저라고 한다.
말이 어려울 수 있으니 우선 하나씩 보도록 하겠다.
내부 함수는 외부 함수의 지역변수에 접근 할 수 있다.
이를 스코프 체인(scope chain)
이라고 한다.
(ES5부터는 스코프 체인이란 단어가 없어져 외부 렉시컬 환경 체인
이라고 부른다.)
function outter(){
const title = 'Hello world';
function inner(){
console.log(title);
}
inner();
}
outter();
함수 outter의 내부에는 함수 inner가 정의 되어 있다. 함수 inner를 내부 함수라고 한다.
스코프 체인으로 인해 내부 함수인 inner에서 외부 함수의 지역변수에 접근할 수 있다.
그렇다면 내부 함수의 실행을 밖에서 한다면 어떻게 될까?
외부 함수가 종료된 뒤에 호출하면 외부 함수의 실행 컨텍스트가 종료되어 내부 함수를 호출하지 못한다.
function outter(){
const title = 'Hello world';
return function(){
console.log(title);
}
}
const inner = outter();
inner();
그러나 위의 코드를 실행해보면 ‘Hello world’이 찍히는 것을 볼 수 있다.
함수 outter를 호출하여 익명 함수가 변수 inner에 담긴다.
outter 함수는 실행이 끝났기 때문에 이 함수의 지역변수는 소멸되는 것이 자연스럽다.
하지만 함수 inner를 실행했을 때 ‘Hello world’가 출력된 것은 외부함수의 지역변수 title이 소멸되지 않았다는 것을 의미한다.
클로저란 내부함수가 외부함수의 지역변수에 접근 할 수 있고,
외부함수는 외부함수의 지역변수를 사용하는 내부함수가 소멸될 때까지 소멸되지 않는 특성을 의미한다.
캡슐화
function makeCounter() {
let count = 0;
return function() {
return count++;
}
}
const conuter = makeCounter();
console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2
- 외부 함수 makeCounter는 내부 함수인 익명 함수의 참조를 반환한다.
이로 인해 내부 함수를 전역 변수 counter가 참조하게 된다. - 내부 함수는 외부 함수 makeCounter의 지역 변수 count를 참조한다.
이로 인해 makeCounter의 렉시컬 환경 컴포넌트를 내부 함수가 참조하게 되고,
이 내부 함수를 전역 변수 counter가 참조하게 되는 것이다.
count는 지역 변수이기 때문에 함수 바깥에서 읽거나 쓸 수 없다.
또한 함수 f가 클로저의 내부 상태를 바꾸는 메서드의 역할을 하고 있다.
클로저의 내부 상태는 외부로부터 숨겨진 상태이다. 함수 f를 통해서만 접근이 가능하다.
이렇게 외부로부터 은폐하는 행위를 가리켜 캡슐화라고 한다.
팩토리 함수
console.log(counter1()); // 0
console.log(counter2()); // 0
console.log(counter1()); // 1
console.log(counter2()); // 1
makeCounter를 실행해서 두 개의 함수를 생성해 보면 모두 별도의 카운터가 된다.
makeCounter를 호출할 때마다 makeCounter의 렉시컬 환경이 새로 생성되기 때문이다.
따라서 클로저는 서로 다른 내부 상태를 저장한다.
클로저를 객체로 간주한다면 makeCounter는 클로저라는 객체를 생성하는 팩토리 함수 역할을 한다.
function makeMultiplier(x) {
return function(y) {
return x * y;
}
}
const multi1 = makeMultiplier(2);
const multi2 = makeMultiplier(10);
console.log(multi1(3)); // 6
console.log(multi1(3)); // 30
반복문 안에서 클로저 만들기
< 잘못된 예시 >
const button = document.getElementRyTagName('button');
for(var i = 0; i < button.length; i++) {
button[i].onclick = function() {
console.log(i);
}
}
<button>0</button>
<button>1</button>
<button>2</button>
for문 앙에서 이벤트 처리기를 등록하는 코드이다.
0을 누르면 0, 1을 누르면 1이 출력될 것이라 예상했겠지만 이 코드를 적용하면 어떤 버튼을 눌러도 3이 출력된다.
이벤트 처리기에 등록한 함수가 전역변수인 i를 참조하는 클로저가 되었기 때문이다.
등록된 함수가 실행되는 시점에는 for문의 실행이 끝나 있기 때문에 3이 계속 출려되게 된다.
그렇다면 의도대로 0, 1, 2가 출력되도록 해보자.
< 즉시 실행 함수 활용 >
for(var i = 0; i < button.length; i++) {
(function(_i) {
button[i].onclick = function() {
console.log(_i);
}
}(i));
}
즉시 실행 함수로 i를 전달해주어 내부 함수에서 그 i를 참조하기 때문에
반복문을 돌 때마다 새로운 렉시컬 환경이 생성되기 때문이다.
< let 활용 >
for(let i = 0; i < button.length; i++) {
button[i].onclick = function() {
console.log(i);
}
}
ES6부터 추가된 let은 블록 유효 범위를 가지고 있다.
따라서 for문의 초기화식에서 선언한 변수를 반복문이 실행될 때마다 새롭게 선언해서 값을 대입하게 된다.
위의 코드와 아래의 코드는 같은 의미이다.
for(var _i = 0; _i < button.length; _i++) {
let i = _i;
button[i].onclick = function() {
console.log(i);
}
}
Reference
https://opentutorials.org/module/532/6544
모던 자바스크립트 입문
https://poiemaweb.com/js-closure