클로저는 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어(하스켈, 리스프,얼랭,스칼라 등)에서 사용되는 중요한 특성이다.
MDN에서 정의하는 클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프(정적 스코프)라 한다.
예시
function outerFunction() {
var outerVariable = 'I am from outerFunction';
function innerFunction() {
console.log(outerVariable);
}
innerFunction(); // innerFunction이 outerFunction 내에서 선언되었으므로 렉시컬 스코프에 의해 innerFunction은 outerVariable에 접근 가능
}
outerFunction(); // 'I am from outerFunction' 출력
함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.
현재 실행중인 컨텍스트는 함수가 호출되었을 때 함수 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장될 참조 값이다. 즉 자신이 존재하는상위 스코프를 기억한다.
const x = 1;
function outer(){
const x = 10;
const inner = function() {console.log(x)};
return inner;
}
// 호출하면 중첩 함수 inner을 반환한ㄷㅣ.
//그리고 outer함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer();
innerFunc(); // 10
outer함수는 inner 을 반환하고 생명주기가 종료된다.
innerFunc을 실행하면 outer함수의 x에 접근이 안될 것 같지만 x가 부활한 것처럼 작동된다.
이처럼 외부함수(outer)보다 중첩 함수(innerFunc)가 더 오래 유지되는 경우 중첩 함수(innerFunc)는 이미 생명 주기가 종료한 외부 함수(outer)의 변수를 참조할 수 있다.
이러한 중첩함수(innerFunc)를 클로저라한다.
클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.
즉, 상태를 완전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.
아래코드는 잘 동작하지만 오류 발생 가능성이 있는 코드이다.
바르게 동작하려면 다음의 전제 조건을 지켜야한다.
//카운트 상태 변수
let num = 0;
//카운트 상태 변경 함수
const increase = function() {
//카운트 상태를 1만큼 증가
return ++num;
};
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
-> increase함수 만이 num 변수를 참조하고 변경할 수 있도록 해야 한다.
//카운트 상태 변경 함수
const increase = function () {
//카운트 상태 변수
let num = 0;
//카운트 상태를 1만큼 증가
return ++num;
}
//이전 상태 유지하지 못한다.
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
increase 함수가 호출될때마다 num이 0으로 초기화되어 상태를 유지하지 못한다.
이전 상태를 유지할 수 있도록 클로저를 사용하면 다음과 같다.
//카운트 상태 변경 함수
const increase = (function () {
//카운트 상태 변수
let num = 0;
//클로저
return function() {
//카운트 상태를 1만큼 증가
return ++num;
};
}());
console.log(increase()); // 1
console.log(increase()); // 2
increase 함수는 클로저를 반환한다.
(클로저는 외부 함수의 변수에 접근할 수 있는 함수임)
외부 함수 내부에 num이라는 변수가 선언, 이 변수는 클로저 내부 함수에서만 접근 가능
num 변수는 현재 카운트 상태를 나타낸다.
내부 함수는 num을 증가시키는 전위 증가 연산자를 사용하여 현재 카운트 값을 1 증가시킨 후 반환
increase 함수가 호출될 때마다 내부 함수가 실행되고, num 값이 증가한다.
캡슐화는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 메서드를
하나로 묶은 것을 말한다.
캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용되기도 하는데 이를 정보 은닉이라한다.
정보 은닉은 외부에 공개할 필요가 없는 구현의 일부를 외부에 공개되지 않도록 감추어
적절치 못한 접근으로부터 객체의 상태가 변경되는 것을 방지해 정보를 보호하고 객체 간의 상호 의존성, 즉 결합도를 낮추는 효과가 있다.
자바스크립트는 public, private, protected와 같은 접근 제한자를 제공하지 않는다.
따라서 기본적으로 외부에 공개되어있다.
아래는 클로저를 사용할때 자주 발생할 수 있는 실수 중 하나는 루프내에서 클로저를 사용할 때 발생하는 문제이다.
function createMultiplier() {
var multipliers = [];
for (var i = 0; i < 5; i++) {
multipliers.push(function(x) {
return x * i;
});
}
return multipliers;
}
var multipliers = createMultiplier();
console.log(multipliers[0](2)); // 예상 결과: 0, 실제 결과: 8
console.log(multipliers[1](2)); // 예상 결과: 2, 실제 결과: 8
console.log(multipliers[2](2)); // 예상 결과: 4, 실제 결과: 8
console.log(multipliers[3](2)); // 예상 결과: 6, 실제 결과: 8
console.log(multipliers[4](2)); // 예상 결과: 8, 실제 결과: 8
이 예제에서는 createMultiplier 함수가 0부터 4까지의 다섯 개의 곱셈 함수를 배열에 추가하는데, 각 곱셈 함수는 클로저입니다. 그러나 모든 클로저는 마지막 반복 시점에서의 i 값(즉, 5)을 기억하게 된다. 따라서 모든 클로저는 i 값이 5일 때의 결과를 반환하게 된다.
이 문제를 해결하기 위해 클로저를 감싸는 또 다른 클로저를 사용하여 각 클로저가 반복될 때의 i 값을 가져온다.
function createMultiplier() {
var multipliers = [];
for (var i = 0; i < 5; i++) {
multipliers.push((function(x) {
return function() {
return x * i;
};
})(i));
}
return multipliers;
}
var multipliers = createMultiplier();
console.log(multipliers[0](2)); // 0
console.log(multipliers[1](2)); // 2
console.log(multipliers[2](2)); // 4
console.log(multipliers[3](2)); // 6
console.log(multipliers[4](2)); // 8
이렇게 수정하면 각 클로저가 생성될 때마다 해당 반복의 i 값을 원하는 결과를 얻을 수 있다.