24장 클로저
클로저
- 클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다
24-01
- 함수가 선언된 렉시컬 환경
const x = 1;
function outerFunc() {
const x = 10;
function innerFunc() {
console.log(x); // 10
}
innerFunc();
}
outerFunc();
24-02
- inner함수가 outer함수의 중첩함수가 아니였다면 x=10을 출력할 수 없었을 것이다
const x = 1;
function outerFunc() {
const x = 10;
innerFunc();
}
function innerFunc() {
console.log(x); // 1
}
outerFunc();
24.1 렉시컬 스코프(24-03)
-
엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정. 이를 렉시컬 스코프(정적 스코프)
-
앞서 살펴 봤듯이 스코프의 실체는 실행 컨텍스트의 렉시컬 환경이다
-
이 렉시컬 환경은 자신의 외부 렉시컬 환경에 대한 참조를 통해 상위 렉시컬 환경과 연결. 이것이 스코프 체인이다
const x = 1;
function foo() {
const x = 10;
bar(); // 호출이 상위 스코프 결정에 영향X 라는 의미
}
function bar() {
console.log(x);
}
foo(); // ? : 1
bar(); // ? : 1
24.2 함수 객체의 내부 슬롯 [Environment]
- 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장
const x = 1;
function foo() {
const x = 10;
// 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
// 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
bar();
}
// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 [[Environment]]에 저장하여 기억한다.
function bar() {
console.log(x);
}
foo(); // ? : 1
bar(); // ? : 1
24.3 클로저와 렉시컬 환경(24-05)
-
(2번 상황)inner 함수는 함수 표현식으로 정의했기 때문에 런타임에 평가(inner 함수의 상위 스코프는 outer 함수의 렉시컬 환경에 바인딩)
-
outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다
-
inner 함수를 호출(4번 상황)하면 inner 함수의 실행 컨텍스트가 생성되고 스택에 푸시
-
외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저 라고 부른다
const x = 1;
// ①
function outer() {
const x = 10;
const inner = function () { console.log(x); }; // ②
return inner;
}
// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer(); // ③
innerFunc(); // ④ 10
24-06
-
이론적으로 모든 함수는 클로저이지만, 모든 함수를 클로저라고 하지않음(아래 예시에서 z는 상위 스코프가 아님)
-
상위 스코프를 참조하지 않기 때문에 foo함수는 아무도 참조 당하지 않고, 스택에서 pop()한 상태여서 가비지 컬렉터가 삭제를 하게된다. 즉, 클로저가 아님(bar함수)
<!DOCTYPE html>
<html>
<body>
<script>
function foo() {
const x = 1;
const y = 2;
// 일반적으로 클로저라고 하지 않는다.
function bar() {
const z = 3;
debugger;
// 상위 스코프의 식별자를 참조하지 않는다.
console.log(z);
}
return bar;
}
const bar = foo();
bar();
</script>
</body>
</html>
24-07
- 순간적으로 참조한다고 해도 이를 클로저라고 하지 않는다
<!DOCTYPE html>
<html>
<body>
<script>
function foo() {
const x = 1;
// 일반적으로 클로저라고 하지 않는다.
// bar 함수는 클로저였지만 곧바로 소멸한다.
function bar() {
debugger;
// 상위 스코프의 식별자를 참조한다.
console.log(x);
}
bar();
}
foo();
</script>
</body>
</html>
24-08
- 아래처럼 클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적
<!DOCTYPE html>
<html>
<body>
<script>
function foo() {
const x = 1;
const y = 2;
// 클로저
// 중첩 함수 bar는 외부 함수보다 더 오래 유지되며 상위 스코프의 식별자를 참조한다.
function bar() {
debugger;
console.log(x);
}
return bar;
}
const bar = foo();
bar();
</script>
</body>
</html>
24.4 클로저의 활용(24-09)
-
클로저는 상태를 안전하게 변경하고 유지하기 위해 사용
-
아래 예시는 num이 전역이라 값이 어디서든지 변경이 될 문제점과 increase 만이 변경가능한게 아닌 문제가 있다
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가 시킨다.
return ++num;
};
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
24-10
- 그렇다고 지역 변수로 넣으면 이전 상태를 유지하지 못한다
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태 변수
let num = 0;
// 카운트 상태를 1만큼 증가 시킨다.
return ++num;
};
// 이전 상태를 유지하지 못한다.
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
24-11
-
클로저를 사용하면 해결
-
추가로 즉시 실행함수를 통해 한 번말 실행되므로 increase 호출될 때마다 num 변수가 재차 초기화될 일은 없을 것이다
// 카운트 상태 변경 함수
const increase = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
// 카운트 상태를 1만큼 증가 시킨다.
return ++num;
};
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
24-12
- 감소도 시켜보자
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
24-13
- 위 예제를 생성자 함수로 표현하면 다음과 같다
const Counter = (function () {
// ① 카운트 상태 변수
let num = 0;
function Counter() {
// this.num = 0; // ② 프로퍼티는 public하므로 은닉되지 않는다.
}
Counter.prototype.increase = function () {
return ++num;
};
Counter.prototype.decrease = function () {
return num > 0 ? --num : 0;
};
return Counter;
}());
const counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
24-14
- 오류를 피하고 안정성을 높이기 위해 클로저를 적극 사용
// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
function makeCounter(aux) {
// 카운트 상태를 유지하기 위한 자유 변수
let counter = 0;
// 클로저를 반환
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
24-15
- 위 예제는 카운터 증감이 연동되지 않으므로 고쳐야 한다 즉시 실행 함수를 사용
// 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 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;
}
// 보조 함수를 전달하여 호출
console.log(counter(increase)); // 1
console.log(counter(increase)); // 2
// 자유 변수를 공유한다.
console.log(counter(decrease)); // 1
console.log(counter(decrease)); // 0
24.5 캡슐화와 정보 은닉(24-16)
- 아래 예제는 메서드를 인스턴스 메서드로 만들어서 객체 생성때마다 중복 생성되는 단점
function Person(name, age) {
this.name = name; // public : this로 해서 가능
let _age = age; // private : 지역변수로 해서 가능
// 인스턴스 메서드
this.sayHi = function () {
console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};
}
const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20.
console.log(me.name); // Lee
console.log(me._age); // undefined
const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30.
console.log(you.name); // Kim
console.log(you._age); // undefined
24-17
- 인스턴스 메서드 -> 프로토타입 메서드 그러나 지역 변수를 참조 못하는 문제가 발생
function Person(name, age) {
this.name = name; // public
let _age = age; // private
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
// Person 생성자 함수의 지역 변수 _age를 참조할 수 없다
console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};
24-18
- 아래 형식으로 생성자 함수를 작성시 해결
const Person = (function () {
let _age = 0; // private
// 생성자 함수
function Person(name, age) {
this.name = name; // public
_age = age;
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};
// 생성자 함수를 반환
return Person;
}());
const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20.
console.log(me.name); // Lee
console.log(me._age); // undefined
const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30.
console.log(you.name); // Kim
console.log(you._age); // undefined
24-19
-
다만, 자바스크립트는 정보 은닉을 완전하게 지원하지 않음(인스턴스가 여러개 생성된 경우 private를 각각 가지지 않고 공유중,,)
-
Person.prototype.sayHi 메서드가 단 한번 생성되는 클로저이기 때문에 발생하는 현상
-
그렇지만 25.7.4절 “private 필드 정의 제안”에서 해결법을 보자
const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20.
const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30.
// _age 변수 값이 변경된다!
me.sayHi(); // Hi! My name is Lee. I am 30.
24.6 자주 발생하는 실수(24-20)
-
0, 1, 2를 반환할것 같지만 아니다
-
for문은 함수 레벨 스코프를 갖기 때문에, i가 결국 3이다
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 반환
}
24-21
- 클로저를 사용해 바르게 동작
var funcs = [];
for (var i = 0; i < 3; i++){
funcs[i] = (function (id) { // ①
return function () {
return id;
};
}(i));
}
for (var j = 0; j < funcs.length; j++) {
console.log(funcs[j]());
}
24-22
- 그런데 let키워드를 사용하면 깔끔하게 해결
const 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
}
24-23
// 요소가 3개인 배열을 생성하고 배열의 인덱스를 반환하는 함수를 요소로 추가한다.
// 배열의 요소로 추가된 함수들은 모두 클로저다.
const funcs = Array.from(new Array(3), (_, i) => () => i); // (3) [ƒ, ƒ, ƒ]
// 배열의 요소로 추가된 함수 들을 순차적으로 호출한다.
funcs.forEach(f => console.log(f())); // 0 1 2
댓글남기기