05 클로저(Closure)
5-1 클로저의 의미 및 원리 이해
- 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다.
- MDN : 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상
- 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상
var outer = function () {
var a = 1;
var inner = function () {
console.log(++a);
};
inner();
};
outer();
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner();
};
var outer2 = outer();
console.log(outer2);
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());
- inner 함수의 실행 시점에는 outer 함수가 이미 실행이 종료된 상태인데 어떻게 outer 함수의 LexicalEnvironment에 접근할 수 있을까? 그 이유는 가비지 컬렉터의 동작 방식 때문. 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다.
- outer 함수는 실행 종료 시점에 inner 함수를 반환한다. 외부 함수인 outer의 실행이 종료되더라도 내부 함수인 inner 함수는 언젠가 outer2를 실행함으로써 호출될 가능성이 있다.
- 함수의 실행 컨텍스트가 종료된 후에도 LE가 가비지 컬렉터의 수집 대상에서 제외되는 경우는 예제와 같이 지역변수를 참조하는 내부함수가 외부로 전달된 경우가 유일하다.
- 클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우, A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상
- setTimeout과 같은 메서드에 전달할 콜백 함수 내부에서 지역변수를 참조하거나, DOM의 메서드인 addEventListener에 등록할 handler 함수 내부에서 지역변수를 참조하는 경우도 클로저이다.
5-2 클로저와 메모리 관리
- 메모리 누수 : 참조 카운트가 0이 되지 않아 GC의 수거 대상이 되지 않는 경우
- 하지만 개발자가 의도적으로 설계한 경우는 메모리 누수라고 할 수 없다.
- 클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수가 메모리를 소모하도록 함으로써 발생함. 그렇다면 필요성이 사라지면 더는 메모리를 소모하지 않게 해주면 된다. ( = 참조 카운트를 0으로 만들어준다. 참조형이 아닌 기본형 데이터, null 이나 undefined를 할당해준다. )
var outer = (function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
})();
console.log(outer());
console.log(outer());
outer = null;
(funtion () {
var a = 0;
var intervalId = null;
var inner = function () {
if (++a >= 10) {
clearInterval(intervalId);
inner = null;
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
(function () {
var count = 0;
var button = document.createElement('button');
button.innerText = 'click';
var clickHandler = function () {
console.log(++count, 'times clicked');
if(count >= 10) {
button.removeEventListener('click', clickHandler);
clickHandler = null;
}
};
button.addEventListener('click', clickHandler);
document.body.appendChild(button);
})();
5-3 클로저 활용 사례
5-3-1 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
fruits.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function () {
alert('your choice is ' + fruit);
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
var alertFruit = function (fruit) {
alert('your choice is ' + fruit);
}
fruits.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit.bind(null, fruit));
$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
var alertFruitBuilder = function (fruit) {
return function () {
alert('your choice is ' + fruit);
};
};
fruits.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListner('click', alertFruitBuilder(fruit));
$ul.appendChild($li);
});
...
5-3-2 접근 권한 제어 (정보 은닉)
- 정보 은닉 (information hiding) : 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높인다.
- 외부에 제공하고자 하는 정보들을 모아서 return하고, 내부에서만 사용할 정보들은 return하지 않는 것으로 접근 권한 제어가 가능
- 자동차 경주 게임
- 각 턴마다 주사위를 굴려 나온 숫자(km)만큼 이동한다.
- 차량별로 연료량(fuel)과 연비(power)는 무작위로 생성된다.
- 남은 연료가 이동할 거리에 필요한 연료보다 부족하면 이동하지 못하낟.
- 모든 유저가 이동할 수 없는 턴에 게임이 종료된다.
- 게임 종료 시점에 가장 멀리 이동해 있는 사람이 승리
var createCar = function () {
var fuel = Math.ceil(Math.random() * 10 + 10);
var power = Math.ceil(Math.random() * 3 + 2);
var moved = 0;
var publicMembers = {
get moved () {
return moved
},
run: function () {
var km = Math.ceil(Math.random() * 6);
var wasteFuel = km / this.power;
if (this.fuel < wasteFuel) {
console.log('이동 불가');
return;
}
this.fuel -= wasteFuel;
this.moved += km;
console.log(`${km}km 이동 (총 ${this.moved}km)`);
}
};
Object.freeze(publicMembers);
return publicMembers;
};
var car = createCar();
- fuel, power, moved 를 외부에서 제어할 수 없도록 클로저를 활용한다.
Object.freeze()
메서드를 통해 동결된 객체가 더 이상 변경될 수 없도록 처리
5-3-3 부분 적용 함수
- 부분 적용 함수 (partially applied function) : 여러개의 인자를 받는 함수에서 미리 일부의 인자만 넘겨 기억시켰다가, 나중에 나머지 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수
var add = function () {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = add.bind(null, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10));
- 위의 add 함수는 this를 사용하지 않으므로 bind 메서드만으로도 구현 가능. 그러나 this의 값을 변경할 수밖에 없기 때문에 메서드에서는 사용할수 없다.
var partial = function () {
var originalPartialArgs = arguments;
var func = originalPartialArgs[0];
if (typeof func !== 'function') {
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function () {
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
var restArgs = Array.prototype.slice.call(arguments);
return func.apply(this, partialArgs.concat(restArgs));
};
};
var add = function () {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = partial(add, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10));
var dog = {
name: '강아지',
greet: partial(function(prefix, suffix) {
return prefix + this.name + suffix;
}, '왈왈, ')
};
dog.greet('입니다!');
- partial 함수는 첫 번째 인자에 원본 함수를, 두 번째 인자 이후부터는 미리 적용할 인자들을 전달하고, 반환할 함수(부분 적용 함수)에서는 다시 나머지 인자들을 모아(concat) 원본 함수를 호출(apply)한다.
- 실행 시점의 this를 그대로 반영함으로써 this에는 영향을 주지 않는다.
- 그러나 인자를 반드시 앞에서부터 차례로 전달할 수 밖에 없다.
var partial2 = function () {
var originalPartialArgs = arguments;
var func = originalPartialArgs[0];
if (typeof func !== 'function') {
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function () {
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
var restArgs = Array.prototype.slice.call(arguments);
for (var i = 0; i < partialArgs.length; i++) {
if (partialArgs[i] === Symbol.for('EMPTY_SPACE')) {
partialArgs[i] = restArgs.shift();
}
}
return func.apply(this, partialArgs.concat(restArgs));
};
};
var add = function () {
...
};
var _ = Symbol.for('EMPTY_SPACE');
var addPartial = partial2(add, 1, 2, _, 4, 5, _, _, 8, 9);
console.log(addPartial(3, 6, 7, 10));
- 디바운스 : 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리. (scroll, wheel, mousemove, resize 등에 적용하기 좋다)
var debounce = function (eventName, func, wait) {
var timeoutId = null;
return function (event) {
var self = this;
console.log(eventName, 'event 발생');
clearTimeout(timeoutId);
timeoutId = setTimeout(func.bind(self, event), wait);
};
};
var moveHandler = function (e) {
console.log('move event 처리');
};
var wheelHandler = function (e) {
console.log('wheel event 처리');
};
document.body.addEventListener('mousemove', debounce('move', moveHandler, 500));
document.body.addEventListener('mousewheel', debounce('wheel', wheelHandler, 700));
5-3-4 커링 함수
- 커링함수 (currying function) : 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성
- 중간 과정상의 함수를 실행한 결과는 그 다음 인자를 받기 위해 대기, 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다.
var curry3 = function (func) {
return function (a) {
return function (b) {
return func(a, b);
};
};
};
var getMaxWith10 = curry3(Math.max)(10);
console.log(getMaxWith10(8));
console.log(getMaxWith10(25));
var getMinWith10 = curry3(Math.min)(10);
console.log(getMinwith10(8));
console.log(getMinwith10(25));
- 인자가 많아질수록 가독성이 떨어진다는 단점이 있다.
- ES6에서는 화살표 함수 사용
var curry5 = func => a => b => c => d => e => func(a,b,c,d,e);
- 각 단계에서 받은 인자들은 모두 마지막 단계에서 참조할 것이므로 GC의 수거대상이 되지 않고 메모리에 쌓여있다가 마지막 호출로 실행 컨텍스트가 종료된 후에야 한꺼번에 GC의 수거대상이 된다.
- 지연실행 (lazy execution) : 당장 필요한 정보만 받아서 전달하며 마지막 인자가 넘어갈 때까지 함수 실행을 미룬다.
var getInformation = function (baseUrl) {
return function (path) {
return function (id) {
return fetch(baseUrl + path + '/' + id);
};
};
};
var getInformation = baseUrl => path => id => fetch(baseUrl + path + '/' + id);
var imageUrl = 'http://imageAddress.com/';
var getImage = getInformation(imageUrl);
var getEmoticon = getImage('emoticon');
var emoticon1 = getEmoticon(100);
const logger = store => next => action => {
console.log('dispatching', action);
console.log('next state', store.getState());
return next(action);
};
const thunk = store => next => action => {
return typeof action === 'function'
? action(dispatch, store.getState)
: next(action);
};
- store, next, action 순서로 인자를 받는다. 이 중 store는 프로젝트 내에서 한 번 생성된 이후로는 바뀌지 않고, dispatch의 의미를 가지는 next도 마찬가지지만 action의 경우는 매번 달라진다.
- store, next를 미리 넘겨서 반환된 함수를 저장시켜놓고, 이후에는 action만 받아서 처리할 수 있게 되어있다.