클로저 처음엔 closer인줄 알았던 클로저.
사실은 closure이라는... 처음알았다
클로저는 그냥 처음부터 난해하다고 설명을 하고있다.
예를 들어보면 이해가 편할수도있다.
const x = 1;
function outerFn() {
const x = 10;
function innerFn() {
console.log(x); //10
}
innerFn();
}
outerFn();
위의 예를 보면 outerFn 내부에서 중첩함수 innerFn이 정의되고 호출되었다.
이때 중첩 함수 innerFn의 상위스코프는 외부 함수 outerFn의 스코프이다.
따라서 innerFn 내부에서 자신을 포함하고 있는 외부함수 outerFn의 x 변수에 접근하여 콘솔에 10이 찍힌다.
const x = 1;
function outerFn() {
const x = 10;
innerFn();
}
function innerFn() {
console.log(x); // 1
}
outerFn();
하지만 위처럼 innerFn 함수가 outerFn 함수 내부에 정의된 중첩 함수가 아니면,
innerFn 함수를 outerFn 함수의 내부에서 호출한다 하더라도 outerFn 함수의 변수에 접근할 수 없다.
즉, 위의 말은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다.
내부 슬롯
함수가 정의된 환경(위치)과 호출된 환경(위치)은 다를 수 있다. 따라서 이를 위해 함수는 자신의 내부 슬롯[[Environment]]에 자신이 정의된 환경, 즉! 상위 스코프의 참조를 저장합니다.
함수 객체의 내부슬롯[[Environment]]에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프이다.
또한, 자신이 호출되었을 때 생성될 함수 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장될 참조값이다.
클로저가 되기위해선
- 외부함수보다 중첩함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다.
- 상위스코프의 식별자를 참조해야한다.
요악해보며나, 함수가 함수를 리턴하고있으면 클로저 의심
리턴되고 있는 함수(내부함수)가 외부함수의 변수를 사용해?? 그럼 클로저!
위의 두가지를 모두 만족해야만 클로저가 될 수 있다.
예를 들어 중첩함수가 외부함수보다 오래 유지는 되지만, 상위 스코프의 어떤 식별자도 참조하지 않으면 클로저라고 할 수 없다.
클로저의 활용
클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.
다시말해, 상태가 의도치않게 변경되지 않도록 상태를 안전하게 은닉하고 특정 함수에게만 상태변경을 허용하기 위해 사용
// 카운트 상태 변경 함수
const increase = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
// 카운트 상태를 1만큼 증가
return ++num;
};
})();
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
위 코드가 실행되면 즉시 실행 함수가 호출, 즉시 실행 함수가 반환한 함수가 increase 변수에 할당.
increase 변수에 할당된 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저.
이처럼 클로저는 상태가 의도치 않게 변경되지 않도록 안전하게 은닉하고,
특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용
심화된 내용으로 카운트상태를 감소시킬 수도 있도록 해보자.
const counter = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저인 메서드를 갖는 객체를 반환.
// 객체 리터럴은 스코프를 만들지 않는다.
// 따라서 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경이다.
return {
// num: 0, // 프로퍼티는 public하므로 은닉되지 않는다
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
위 예제에서 즉시 실행 함수가 반환하는 객체 리터럴은 즉시 실행 함수의 실행 단계에서 평가되어 객체가 된다.
이때 객체의 메서드도 함수 객체로 생성된다. 객체 리터럴의 중괄호는 코드 블록이 아니므로 별도의 스코프를 생성하지않는다.
위의 increase, decrease 메서드의 상위 스코프는 두개의 메서드가 평가되는 시점에 실행중인 실행 컨텍스트인
즉시 실행 함수 실행 컨텍스트의 렉시컬 환경이다.
따라서 두개의 메서드가 언제 어디서 호출되든 즉시 실행 함수의 스코프의 식별자를 참조할 수 있다.
클로저 사용에서의 실수
다음은 클로저를 사용할 때 자주 발생하는 실수를 예제이다.
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () {
return i;
};
}
for (var j = 0; j < funcs.length; j++) {
console.log(funcs[j]()); // 3 3 3
}
위의 코드를 보면 for문의 변수 선언문에서 var키워드로 선언한 변수 i 는 블록 레벨 스코프가 아닌 함수 레벨 스코프를 갖기 때문에 전역변수이다. 따라서 funcs 배열의 요소로 추가한 함수를 호출하면 전역 변수 i를 참조하여 i의 값 3이 출력된다.
하지만, var키워드를 사용하지 않고 let 키워드를 사용하면, var 키워드로 선언한 변수가 전역 변수가 되는걸 막기 때문에 해결이 된다.
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
return i;
};
}
for (let j = 0; j < funcs.length; j++) {
console.log(funcs[j]()); // 0 1 2
}
for문의 변수 선언문에서 let 키워드로 선언한 변수를 사용하면 for문의 코드 블록이 반복 실행될 때마다
for문 코드블록의 새로운 렉시컬 환경이 생긴다.
이때 함수의 상위 스코프는 for문의 코드블록이 반복 실행될때 마다 식별자의 값을 유지해야 한다.
이를 위해 for문이 반복될 때마다 독립적인 렉시컬 환경을 생성하여 식별자의 값을 유지한다.