※ 본 게시글은 모던자바스크립트 deep dive를 보고 정리한 내용입니다. 틀린 내용이 있다면 댓글로 적어주시면 감사드리겠습니다 (__)
클로저에 대해 공부하고 클로저에 대해 간단히 설명을 해보자면
- 둘러싸여진 상태의 참조와 함께 다발로 묶여진 함수의 콤비네이션
- 내부함수로부터 외부함수의 접근 권한을 줌
- 함수 생성 시점에 언제나 생김
작성한 글을 쭉 읽어보면 무슨 말을 하고 싶은 건지 대강 이해가 될것이다.
렉시컬 스코프
함수를 어디에 정의했는지에 따라 상위 스코프를 결정 -> 렉시컬(정적) 스코프
렉시컬 환경의 '외부 렉시컬 환경에 대한 참조' 에 저장할 참고값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경에 의해 결정된다. 이것이 바로 렉시컬 스코프이다.
함수 객체의 내부 슬롯 [[Environment]]
함수는 자신의 내부 슬롯[[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.
클로저와 렉시컬 환경
const x = 1;
function outer() {
const x = 10;
const inner = function () { console.log(x); };
return inner;
}
const innerFunc = outer();
innerFunc(); // 10
outer함수를 호출하면 inner를 반환하고 실행 컨텍스트에서 제거되는데 innerFunc() 호출 시, 결과로 10이 나오게 된다. 이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다.
outer함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer함수의 렉시컬 환경 까지 소멸하는 것은 아니다.
클로저의 활용
클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다. 즉 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.
const increase = (function () {
let num = 0;
// 클로저
return function () {
return ++num;
};
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
이 예제에서 즉시 실행 함수가 반환한 클로저는 increase 변수에 할당되어 호출되어서 즉시 실행 함수의 렉시컬 환경을 상위 스코프로 가지고 있기 때문에 num에 접근하여 참조하고 변경할 수 있다.
const Counter = (function () {
let num = 0;
function Counter() {
}
Counter.prototype.increase = function() {
return ++num;
};
Counter.prototype.decrease = function() {
return num > 0 ? --num : 0;
};
return Counter;
}());
const counter = new Counter();
인스턴스의 프로퍼티가 아닌 즉시 실행 함수 내부에 num을 정의해두고 프로토 타입을 통해 increase, decrease 메서드를 상속받는 인스턴스를 생성한다. 이 메서드는 즉시 실행 함수 실행 컨텍스트의 렉시컬 환경을 기억하는 클로저이어서 자유 변수 num을 참조할 수 있다.
함수형 프로그래밍에서 클로저를 활용하기 위해서는 일반 함수로 만들어서 함수를 전달받고 함수를 반환하는 상위 함수로 만들게 되면 보조 함수에 따라 여러번 호출할 수 있어서 함수를 반환할 때 반환된 함수는 자신만의 독립된 렉시컬 환경을 갖는다. --> 카운터 증감이 연동이 되지 않을 수 있다.
따라서 독립된 카운터가 아닌 연동하여 증감이 가능한 카운터를 만들려면 렉시컬 환경을 공유하는 클로저를 만들어야 한다. 다음 코드를 살펴보자
const counter = (function() {
let counter = 0;
// 함수를 인수로 전달받는 클로저 반환
return function (predicate) {
// 인수로 전달받은 보조 함수에 상태 변경 위임
counter = predicate(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
* predicate 에 관한 내용은 ==> https://livelikeabel.tistory.com/86
캡슐화와 정보 은닉
캡슐화는 객체지향 특성 중 하나이다.
캡슐화: 프로퍼티와 메서드를 하나로 묶는 것. 보통 특정 프로퍼티나 메소드를 감출 목적으로 사용하기도하는데 이를 정보 은닉이라고 한다.
생성자 함수안에 지역변수로 선언한 변수는 외부에서 참조하거나 변경할 수 없는데 이 변수를 참조하는 메서드를 프로토타입으로 만든 경우 보통 방법으론 이 변수에 접근을 할 수 없다. --> 즉시 실행함수를 사용하여 생성자 함수와 prototype에 정의할 메서드를 한곳에 모아서 선언.
하지만 이 방법도 생성자 함수가 여러 개의 인스턴스를 생성할 경우 그 변수의 상태가 유지 되지 않는다.
(프로토타입 메소드가 단 한번 생성되는 클로저 이기 때문에 하나의 동일한 상위 스코프를 사용한다.)
클로저를 사용하면서 사용하는 실수
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번 출력된다.
왜냐하면 for문 안에 var 키워드로 선언한 i 변수가 함수레벨 스코프이기 때문에 전역변수 취급을 하기 때문이다.
그렇다면 0, 1, 2 를 출력하기 위해 클로저를 활용한 코드는 어떻게 작성하면 될까?
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]());
}
i를 인수로 받아 매개변수 id에 할당한 후 중첩 함수를 반환하고 종료된다. 매개 변수 id는 즉시 실행 함수가 반환한 중첩 함수의 상위 스코프에 존재하기 때문에 자유 변수가 되어 그 값이 유지된다.
var 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]());
}
for문 안 var 키워드를 let 키워드로 바꿔줘도 된다.
'Programming > JavaScript' 카테고리의 다른 글
[JavaScript] this 바인딩 (0) | 2022.03.29 |
---|---|
[JavaScript] Array.from (0) | 2022.03.25 |
[JavaScript] '?' 옵셔널 체이닝 연산자 (0) | 2022.03.24 |
[JavaScript] prototype 정리 (0) | 2022.03.24 |
[JavaScript] 가비지콜렉션, WeakRef (0) | 2022.03.21 |