클로저
시작
이어서 클로저에 대해 알아보겠습니다.
클로저
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합입니다.
const x = 1;
function outerFunc() {
const x = 10;
function innerFunc() {
console.log(x); // 10 스코프 체인에 의해 innerFunc내(현재 record) x가 없으므로
// outer 즉, outerFunc의 x를 참조한다.
};
innerFunc;
}
outerFunc();
단 다음과 같은 경우는 선언 부분에 x는 1이므로 1이 출력된다. 자바스크립트는 렉시컬 프로그래밍을 따르기 때문이다.
const x = 1;
function outerFunc() {
const x = 10;
innerFunc();
}
function innerFunc() {
console.log(x) // 1
}
outerFunc();
렉시컬 스코프
JS엔진은 함수를 어디서 호출했는지가 아닌 함수를 어디에 정의했는지에 따라 상위 스코프가 결정된다.
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x)
}
foo(); // 1
bar(); // 1
다시 말해 외부 렉시컬 환경에 대한 참조에 저장할 참조값, 즉, 스코프에 대한 참조는 함수 정의
가 평가되는 시점 함수가 정의된 환경(위치)
에 의해 결정된다. 이것이 바로 렉시컬 스코프이다. 정의된 환경에 대한 정보는 reocrd가 아닌 outer에 저장된다.
클로저와 렉시컬 환경과
외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 여전히 참조할 수 있다. 즉 이 개념에서 중첨 함수가 바로 클로저
이다.
const x = 1;
function outer() {
const x = 10;
const inner = function() {console.log(x)};
return inner;
}
const innerFunc = outer();
// outer는 콜 스택에서 제거된다. 단, 렉시컬 환경은 잔존하여 inner가 실행될 때 이 환경을 참조한다.
innerFunc();
// outer 함수를 호출하면 중첩 함수 inner를 반환
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스탭에서 팝되어 제거(역할을 마쳤으므로)
// inner 함수는 런타임에 평가된다. 함수 표현식이므로.
// inner 함수가 innerFunc에 전달되었는데, 이는 outer 함수의 렉시컬환경을 참조하고 있다.
// 즉, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다.
가비지 콜렉터
가비지 콜렉터는 안쓰는 즉 참조되지 않는 메모리 공간은 언젠가 비워둔다. 그럼 위 코드는 어떨가 outer함수의 x도 삭제할까? 아니다. inner가 outer의 렉시컬 환경을 참조하기 때문에 x는 사라지지 않는다.
클로저와 아닌 것의 구분이
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();
클로저의 활용
클로저는 주로 상태를 안전(은닉)하게 변경하고 유지하기 위해 사용합니다. 또한 의도치 않는 상태 변경을 막기 위해 사용합니다.
다음은 카운트 예제입니다.
// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
// 아래와 같이 잘 동작하지만 값이 은닉되지 않아 즉, 상태가 은닉되지 않아 중간에 변경이 가능하다.
console.log(increase()); // 1
num = 100; // 치명적인 단점이 있다.
console.log(increase()); // 100
console.log(increase()); // 101
// 보완해야 할 사항
// (1) 카운트 상태(num 변수의 값)는 increase 함수가 호출되기 전까지 변경되지 않고 유지돼야 한다.
// (2) 이를 위해 카운트 상태(num 변수의 값)는 increase 함수만이 변경할 수 있어야 한다.
// 전역변수인 num이 문제다 문제!! -> 지역변수로 바꿔볼가...?
위 코드의 경우 상태가 은닉되지 않는다. 아래 코드를 보자. 아래 코드는 은닉이 된다.
// 카운트 상태 변경 함수
const increase = (function () { // 즉시 실행 함수 {}()를 붙이게 되면 즉시 실행 함수 {}가 increase에 담긴다. 즉 increase 호출 시 즉시 {}구문이 실행된다.
// 카운트 상태 변수
let num = 0;
// 클로저 이 구문이 반환되어 increase에 즉시 실행 함수로 담기게 됨.
// 실행할 때마다 상위 스코프의 렉시컬 환경을 저장하기 때문에 상태를 유지하면서 은닉할 수 있다.
return function () {
return ++num;
}
}());
// 이전 상태값을 유지
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3
// 코드 설명
// 1. 위 코드가 실행되면 즉시 실행 함수가 호출되고 즉시 실행 함수가 반환한 함수가
// increase 변수에 할당된다.
// 2. increase 변수에 할당된 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프인
// 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저다.
// 3. 즉시 실행 함수는 호출된 이후 소멸되지만, 즉시 실행 함수가 반환한 클로저는 increase
// 변수에 할당되어 호출된다.
// 4. 이때 즉시 실행 함수가 반환한 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프인
// 즉시 실행 함수의 렉시컬 환경을 기억하고 있다.
// 5. 따라서 즉시 실행 함수가 반환한 클로저는 카운트 상태를 유지하기 위한 자유 변수 num을
// 언제 어디서 호출하든지 참조하고 변경할 수 있다.
// 6. num은 초기화되지 않을 것이며, 외부에서 직접 접근할 수 없는 은닉된 private 변수이므로,
// 전역 변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요도 없다.
// 결론 : 클로저는 상태(state)가 의도치 않게 변경되지 않도록 안전하게 은닉(information hiding)
// 하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경
// 하고 유지하기 위해 사용한다.
아래코드는 감소 기능도 추가한 코드이다.
// 클로저 카운트 기능 확장(값 감소 기능 추가)
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
// 8번째 줄에서 { 가 있는데, 별도의 스코프를 생성하진 않나요?
지금까지 했던 프로그래밍은 함수형 프로그래밍 기법이다.
이 기법은 불변성을 지향하며 클로저를 활용해 예상치 못한 외부 영향 오류를 피하고 안정성을 높일 수 있다.
아래는 함수형 프로그래밍에서 클로저를 활용하는 예시 코드이다.
// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
function makeCounter(aux) {
// 카운트 상태를 유지하기 위한 자유 변수
let counter = 0;
// counter를 기억하는 클로저를 반환
return function () {
//인수로 전달받은 보조 함수에 상태 변경을 위임한다.
counter = aux(counter); // 즉시 실행 함수를 저장해 반환
return counter;
};
}
// 보조 함수
function increase(n) {
return ++n;
}
// 보조 함수
function decrease(n) {
return --n;
}
// 함수로 함수를 생성
// makeCounter함수는 보조 함수를 인수로 전달받아 함수를 반환한다.
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
// 그럼 어떻게 해야 하나요?
// -> key : makeCounter 함수를 1번만 호출할 것
2번째 코드
// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
const counter = (function(){
// 카운트 상태를 유지하기 위한 자유 변수
let counter = 0;
// 클로저를 반환
return function (aux) {
//인수로 전달받은 보조 함수에 상태 변경을 위임한다.
counter = aux(counter);
return counter;
};
}());
// 보조 함수
function increase(n) {
return ++n;
}
// 보조 함수
function decrease(n) {
return --n;
}
// 함수로 함수를 생성
// makeCounter함수는 보조 함수를 인수로 전달받아 함수를 반환한다.
console.log(counter(increase)); // 1
console.log(counter(increase)); // 2
// 자유 변수를 공유
console.log(counter(decrease)); // 1
console.log(counter(decrease)); // 0
캡슐화(enCapsulation)와 정보 은닉
캡슐화는 프로퍼티와 메서드를 하나로 묶고 객체의 특정 프로퍼티, 메서드를 감출 목적으로 사용한다. 다음 생성자 함수를 보자.
function Person(name, age) {
this.name = name; //public 외부 참조 가능.
let _age = age; //private 외부 참조 불가. 클로저내에서만 제어 가능
//인스턴스 메서드
//따라서, Person 객체가 생성될 때 마다 중복 생성됨 : 해결방법 -> prototype
this.sayHi = function () {
console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};
}
const me = new Person('Choi', 33);
me.sayHi(); // Hi!, My name is Choi. I am 33.
console.log(me.name); // Choi
console.log(me._age); // undefined
const you = new Person('Lee', 30);
you.sayHi(); // Hi! My name is Lee. I am 30.
console.log(you.name); // Lee
console.log(you.age); // undefined
다음과 같이 블록 레벨 스코프를 지원하는 let을 사용한다면 해당 블록 스코프의 렉시컬 환경을 참조하면서 원하는 값. 1 2 3을 출력하는 코드이다. 함수 레벨 스코프를 지원하는 var를 사용한다면 아래와 같은 출력은 기대할 수 없다.
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function() { return i; };
}
for (let i = 0; i < funcs.length; i++) {
console.log(funcs[i]()); // 0 1 2
}