24장 클로저
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
렉시컬 스코프
JS 엔진은 함수를 어디에 정의했는지에 따라 상위 스코프를 결정하고, 이를 렉시컬 스코프
라고 한다.
즉, 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경에 의해 결정된다.
함수 객체의 내부 슬롯 [[Environment]]
함수가 정의된 환경과 호출되는 환경은 다를 수 있음. 렉시컬 스코프를 따르므로 호출되는 환경과 상관없이 자신이 정의된 환경을 기억하고 있어야한다.
함수는 자신의 내부 슬롯 [[Environment]] 에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.
코드 평가 시점 중에 코드 내의 함수 선언문이 평가되어 함수 객체를 생성하는데, 이때 [[Environment]] 에 현재 실행중인 실행 컨텍스트의 렉시컬 환경의 참조가 저장되는 것.
함수가 호출되면 렉시컬 환경의 외부 렉시컬 환경에 대한 참조
에 해당 참조값이 저장된다.
const x = 1;
function foo() {
const x = 10;
// 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
// 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
bar();
}
// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 [[Environment]]에 저장하여 기억한다.
function bar() {
console.log(x);
}
foo(); // ?
bar(); // ?
복습하다가 외부 렉시컬환경 참조가 있는데 왜 [[Enviroment]] 가 필요하지 라고 생각라는 생각이 들었다. 위에 잘 읽어보면 나오지만, 또 다시 헷갈리지 않기 위해 정리.
함수 객체가 생성될 때는 평가 될 때고 실행될 때 함수의 렉시컬 환경이 생성된다. 근데 이때 외부 렉시컬 환경 참조를 알수가 없기 때문에 스코프 체인 따라가서 함수 객체 찾고 함수객체의 [[Environment]] 를 가져오는것
클로저와 렉시컬 환경
const x = 1;
// ①
function outer() {
const x = 10;
const inner = function () { console.log(x); }; // ②
return inner;
}
// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer(); // ③
innerFunc(); // ④ 10
위 코드에서 outer의 호출이 끝나고 실행 컨텍스트가 할당 해제된 것 처럶 보이지만, 지역 변수 x에 대한 접근이 가능하다.
이처럼 외부함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 생명 주기가 종료된 외부함수의 변수를 참조할 수 있다. 이러한 중첩함수를 클로저
라고 부른다.
outer 함수의 실행이 종료하면 inner 함수를 반환하면서 생명주기가 끝난다. 이때, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 렉시컬 환경은 사라지지 않는다.
왜 why?
전역 변수 innerFunc 에 의해서 inner 함수가 참조되고 있고, inner 함수의 [[Environment]] 에서 outer 함수의 렉시컬 환경을 참조하기 때문에.
JS 의 모든함수는 상위 스코프를 기억하기 때문에 이론적으로는 모든 함수는 클로저라고 볼 수도 있다. 하지만 모두 클로저라고 부르지는 않는다.
⇒ 상위 스코프의 어떤 식별자도 참조하지 않는 경우 클로저라고 하지 않는다. 이 경우 브라우저가 최적화를 통해서 상위 스코프를 기억하지 않는다. 즉, [[Environment]] 를 기억하지 않는다는 말.
⇒ 중첩함수가 반환되지 않아서 외부함수보다 중첩함수의 생명주기가 짧은 경우도 클로저라고 하지 않는다.
❓ 그러면 뭐가 클로전디?
<!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>
이처럼 중첩함수가 상위 스코프의 식별자를 참조하고 있고 중첩함수가 외부함수보다 더 오래 유지되는 경우에 한정해서 클로저
라고 한다.
클로저의 활용
클로저는 상태를 안전하게 변경하고 유지하기 위해 사용.
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가 시킨다.
return ++num;
};
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
카운트 상태 변수를 전역변수를 사용해서 암묵적 결합이 발생할 수 있고 의도치 않게 변경될 수 있다.
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태 변수
let num = 0;
// 카운트 상태를 1만큼 증가 시킨다.
return ++num;
};
// 이전 상태를 유지하지 못한다.
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
그렇다고 카운트를 지역변수로 사용하면 매 호출시 마다 초기화 되기 때문에 이전의 상태를 유지하지 못한다.
// 카운트 상태 변경 함수
const increase = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
// 카운트 상태를 1만큼 증가 시킨다.
return ++num;
};
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
즉시 실행 함수의 렉시컬 환경에 increase 만 접근할 수 있다.
즉, 자원에 대한 접근이 제한되고 상태를 기억할 수 있다는 말이다.
이렇게 클로저
는 상태가 의도치 않게 변경되지 않도록 은닉하고 특정 함수에게만 상태 변경을 가능하게하여 상태를 안전하게 변경하고 유지할 수 있다.
➕ 프로퍼티는 public 해서 은닉되지 않는다.
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
- 위 코드를 생성자 함수로 표현하면 다음과 같다.
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
// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 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
왜 연동되지 않을까?
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;
}
// 보조 함수를 전달하여 호출
console.log(counter(increase)); // 1
console.log(counter(increase)); // 2
// 자유 변수를 공유한다.
console.log(counter(decrease)); // 1
console.log(counter(decrease)); // 0
캡슐화와 정보은닉
캡슐화
는 객체의 상태를 나타내는 프로퍼티와 그 프로퍼티를 주무룰 수 있는 메서드를 하나로 말한다. 프로퍼티나 메서드를 감출 목적으로 사용되기도 하는데 이를 정보은닉
이라고 한다.
JS에서는 다른 객체지향 언어와 달리 public, private, protected와 같은 접근 제한자를 제공하지 않는다. 기본적으로 객체의 모든 프로퍼티와 메서드는 public하다.
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}.`);
프로토타입 메서드에서 지역변수 _age
에 접근하지 못한다.
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
다음 처럼 클로저 이용해서 할 수 있어 보이지만, 여러개의 인스턴스 생성하면 age 의 값이 유지가 안됨.
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.
⇒ JS 로 완벽하게 private한 프로퍼티 구현하기 힘듬.
private 필드 정의 제안이란게 있다고는 함.
자주 발생하는 실수
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]()); // ②
}
출력결과가 0, 1, 2
가 아니라 3, 3, 3
이 출력된다
왜 why?
i를 var로 선언해서 i가 전역변수로 선언되어서, i 값인 3이 출련된다.
함수의 실행은 호출되는 순간에 이루어지기 때문에 return i
가 2번에서 실행되기 때문에 그 순간의 i 값인 3인 출력된다.
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]());
}
이런 식으로 클로저를 이용해서 해결할 수도 있다.
💡 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
}
for문에서 반복할 때마다 독립적인 블록 렉시컬 환경을 만든다고 했다.
let은 블록 단위 스코프를 가지기 때문에 for 문 안에서 지역 변수가 되고 즉, for문에서 만든 블록 렉시컬 환경의 환경 레코드에 등록이 된다. var로 선언할 경우 전역 변수가 된다.
funcs[i]는 각각의 for문에서 생성한 블록 렉시컬 환경에서 생성되었다. 따라서 각 funcs[i] 함수 객체의 내부슬롯 [[Environment]] 에는 각 블록 렉시컬 환경이 참조된다.
⇒ 블록 렉시컬 환경이 참조되어져서. 가비지 컬렉션이 일어나지 않게 된다. 외부에서 참조되지 않았다면 그대로 가비지 컬렉션이 되었을 것.
💡 또 다른 방법으로 고차 함수를 사용하는 방법이 있다고 한다.. 아직 참고만.
// 요소가 3개인 배열을 생성하고 배열의 인덱스를 반환하는 함수를 요소로 추가한다.
// 배열의 요소로 추가된 함수들은 모두 클로저다.
const funcs = Array.from(new Array(3), (_, i) => () => i); // (3) [ƒ, ƒ, ƒ]
// 배열의 요소로 추가된 함수 들을 순차적으로 호출한다.
funcs.forEach(f => console.log(f())); // 0 1 2