- MDN 정의: 클로저란 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상
- 선언될 당시의 lexical environment: 실행 컨텍스트의 구성 요소 중 하나인 outerEnvironmentReference에 해당함.
- LexicalEnvironment의 environmentRecord와 outerEnvironmentReference에 의해 변수의 유효범위인 스코프가 결정되고, 스코프 체인이 가능
단, 내부함수 B에서 외부 변수 A를 참조하는 경우에만 해당됨. 외부 변수를 참조하지 않으면 B가 A의 LexicalEnvironment를 사용할 수 없음!
외부 함수의 변수를 참조하는 내부 함수 (1)
var outer = function () {
var a = 1;
var inner = function () {
// inner 함수에는 a 변수가 없으므로, outerEnvironmentReference에서 outer 변수의 a 참조
console.log(++a); // 2
};
inner();
};
outer();
외부 함수의 변수를 참조하는 내부 함수 (2)
var outer = function () {
var a = 1;
var inner = function () {
return ++a; // 2
};
return inner(); // inner 함수 실행 결과를 리턴함.
// 즉, outer 함수의 실행 컨텍스트가 종료된 시점에는 a 변수를 참조하는 대상이 없어짐
// a, inner 변수 값들은 언젠간 가비지 컬렉터에의해 소멸
};
var outer2 = outer();
connsole.log(outer2); // 2
외부 함수의 변수를 참조하는 내부 함수 (3)
var outer = function () {
var a = 1;
var inner = function () {
return ++a; // 2
};
return inner; // (1) inner 함수 실행 결과가 아닌, 'inner 함수 자체'를 리턴함.
};
var outer2 = outer(); // (2)
connsole.log(outer2); // (3) 2
connsole.log(outer2); // (4) 3
즉, 위 예시를 통해 다시 클로저의 정의를 확인할 수 있음:
메모리 누수: 개발자의 의도와 달리 어떤 값의 참조 카운트가 0이 되지 않아 가비지 컬렉팅의 수거 대상이 되지 않는 경우
클로저의 메모리 관리 방법:
// return 에 의한 클로저의 메모리 해제
var outer = (function () {
var a = 1;
var inner = function () {
return ++a;
}
return inner;
})();
console.log(outer());
console.log(outer());
outer = null; // outer 식별자의 inner 함수 참조를 끊음
const fruits = ['apple', 'banana', 'peach'];
const $ul = document.createElement('ul'); // (공통 코드)
fruits.forEach(function (fruit) { // (A) forEach 콜백
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function() {// (B) 클릭 이벤트 핸들러
alert('your choice is ' + fruit); // fruit 외부 변수 참조
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
var alertFruit = function (fruit) { // (1)
alert('your choice is ' + fruit);
}
fruits.forEach(function (fruit) {
const $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit.bind(null, fruit)); // (2)
$ul.appendChlid($li);
});
document.body.appendChild($ul);
const alertFruitBuilder = function (fruit) { // 외부 함수
return function () { // 내부 함수 (1)
alert('your choice is ' + fruit); // 외부 변수 참조
};
};
fruits.forEach(function (fruit) {
const $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruitBuilder(fruit)); (2)
$ul.appendChild($li);
});
클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하는 것이 가능하다.
const outer = function () {
let a = 1;
const inner = function () {
return ++a;
};
return inner; // (1)
};
const outer2 = outer();
console.log(outer2());
console.log(outer2());
클로저를 활용해 접근권한을 제어하는 방법은 다음과 같다:
부분 적용 함수 구현 1)
const partial = function(){
const originalPartialArgs = arguments;
const func = originalPartialArgs[0];
if(typeof func !== 'function'){
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function(){
// 외부함수의 areguments를 함수 빼고 두 번째 인자부터 배열로 만듬
// 첫 번째 인자 배열
const partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
// 두 번째 인자 배열
const restArgs = Array.prototype.slice.call(arguments); // 현재 함수의 arguments
return func.apply(this, partialArgs.comcat(restArgs)); // 인자를 합쳐서 func 실행
// 여러 개의 인자들을 하나의 배열로 보냄(apply) func에서는 배열 자체가 아니라 배열 속 원소들을 인자로 받음
};
};
const add = function() {
let result = 0;
for (let i = 0; i < arguments.length; i++){
result += arguments[i];
}
return result;
}
const addPartial = partial(add, 1,2,3,4,5);
console.log(addPartial(6,7,8,9,10)); // 55
const dog = {
name: '강아지',
greet: partial(function(prefix, suffix) {
return prefix + this.name + suffix;
}, '왈왈, ')
}
dog.greet('입니다!'); // 왈왈, 강아지 입니다!
하지만, 위 예시의 경우 반드시 인자를 앞에서부터 차례로 전달할 수 밖에 없다. 인자들을 원하는 위치에 미리 넣어놓고 나중에 빈 자리에 인자를 채워넣어 실행할 수 있도록 코드를 수정해보자:
부분 적용 함수 구현 2)
Object.defineProperty(window, '_', {
value: 'EMPTY_SPACE',
writable: false,
configurable: false,
enumerable: false
});
const partial12 = functino(){
const originalPartialArgs = arguments;
const func = originalPartialArgs[0];
if(typeof func !== 'function'){
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function(){
const partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
const restArgs = Array.prototype.slice.call(arguments);
// (**) 추가된 for문. 비어있으면(_) restArgs 인자들 차례대로 끼워 넣기
for (var i = 0; i < partialArgs.length; i++){
if(partialArgs[i] === _) {
partialArgs[i] = restArgs.shift();
}
}
return func.apply(this, partialArgs.concat(restArgs));
};
};
const addPartial2(add, 1,2,_,4,5,_,_,8,9);
console.log(addPartial(3,6,7,10)); //55
부분 적용 함수3) 디바운스
const debounce = function(eventName, func, wait){
let timeoutId = null;
return function (event) {
const self = this;
console.log(eventName, 'event 발생');
clearTimeout(timeoutId); // (2)
timeoutId = setTimeout(func.bind(self, event), wait); // (1)
};
};
const moveHandler = function (e) {
console.log('move event 처리');
console.dir(e); // MouseEvent 객체
};
const wheelHandler = function (e) {
console.log('wheel event 처리');
}
//이벤트가 발생하면 debounce 함수가 실행되고, 내부함수를 리턴, 반환된 내부함수가 이벤트 핸들러가 된다.
document.body.addEventListener('mouse', debounce('move', moveHandler, 500));
document.body.addEventListener('mouse', debounce('move', moveHandler, 700));
커링 함수 1)
const curry3 = function(func) {
return function(a) {
return function(b){
return func(a,b);
}
}
};
const getMaxWith10 = curry3(Math.max)(10);
console.log(getMaxWith10(8)); // 10
console.log(getMaxWith10(25)); // 25
const getMinWith10 = curry3(Math.min)(10);
onsole.log(getMinWith10(8)); // 8
console.log(getMinWith10(25)); // 10
커링 함수 2)
const curry5 = function(func) {
return function(a) {
return function(b) {
return function(c) {
return function(d) {
return function(e) {
return func(a,b,c,d,e);
};
};
};
};
};
};
const getMax = curry5(Math.max);
console.log(getMax(1)(2)(3)(4)(5));// 11
// ES6 화살표함수
const curry5 = func => a => b => c => d =>e => func(a,b,c,d,e);
커링 함수3)
// ES6 (서버에 요청할 주소의 기본 url, path값, id 값, 실제 서버에 정보 요청(fetch) )
const getInformation = baseUrl => path => id => fetch(baseUrl + path + '/' +id);
//url 전달
const ImgUrl = 'http://imageAddress.com/';
const getImg = getInformation(ImgUrl);
//path전달
const getEmoticon = getImg('emoticon');
const getIcon = getImg('icon');
//실제요청 (마지막 id만전달) -> 원본함수실행
const emoticon1 = getEmoticon(100);
const emoticon2 = getEmoticon(102);
const icon1 = getIcon(205);
const icon2 = getIcon(234);
const icon3 = getIcon(265);
최근의 여러 프레임워크나 라이브러리 등에서 커링을 광범위하게 사용하고 있다. Flux 아키텍처의 구현체 중 하나인 Redux의 미들웨어를 예로 들면 다음과 같다.
Redux의 미들웨어의 커링 함수
// Redux Middleware 'Logger'
const loger = store => next => action => {
console.log('dispatching', action);
console.log('next state', store.getState());
return next(action);
}
// Redux Middleware 'thunk'
const thunk = store => next => action => {
return typeof action === 'function'
? action(dispatch, store.getState)
: next(action);
- 정재남, 『코어 자바스크립트』, 위키북스(2019), p115-145.
- 5-3 클로저의 활용 사례(외부 데이터 사용, 접근 권한 제어)
- [JS]클로저 활용예시 (부분적용함수, 커링함수)