Core Javascript 05 클로저
67637 단어 core javascript클로저core javascript
05 클로저(Closure)
5-1 클로저의 의미 및 원리 이해
- 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다.
- MDN : 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상
- 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상
var outer = function () {
var a = 1;
var inner = function () {
console.log(++a); // 내부에서는 a를 선언x, outer의 LE에서 a를 찾는다.
};
inner();
};
outer();
// outer 함수의 실행 컨텍스트가 종료되면 a, inner는 GC의 수집 대상이 된다.
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner(); // inner 함수를 실행한 결과를 리턴
};
var outer2 = outer(); // outer 함수의 실행 컨텍스트가 종료되면 a 변수를 참조하는 대상이 없어짐
console.log(outer2); // 2
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner; // inner 함수 자체를 반환
};
var outer2 = outer(); // outer2 변수는 outer의 실행 결과인 inner 함수를 참조
console.log(outer2()); // 2
console.log(outer2()); // 3
var outer = function () {
var a = 1;
var inner = function () {
console.log(++a); // 내부에서는 a를 선언x, outer의 LE에서 a를 찾는다.
};
inner();
};
outer();
// outer 함수의 실행 컨텍스트가 종료되면 a, inner는 GC의 수집 대상이 된다.
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner(); // inner 함수를 실행한 결과를 리턴
};
var outer2 = outer(); // outer 함수의 실행 컨텍스트가 종료되면 a 변수를 참조하는 대상이 없어짐
console.log(outer2); // 2
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner; // inner 함수 자체를 반환
};
var outer2 = outer(); // outer2 변수는 outer의 실행 결과인 inner 함수를 참조
console.log(outer2()); // 2
console.log(outer2()); // 3
- 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를 할당해준다. )
// (1) return에 의한 클로저의 메모리 해제
var outer = (function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
})();
console.log(outer()); // 2
console.log(outer()); // 3
outer = null; // outer 식별자의 inner 함수 참조를 끊음
// (2) setInterval에 의한 클로저의 메모리 해제
(funtion () {
var a = 0;
var intervalId = null;
var inner = function () {
if (++a >= 10) {
clearInterval(intervalId);
inner = null; // inner 식별자의 함수 참조를 끊음
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
// (3) eventListener에 의한 클로저의 메모리 해제
(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; // clickHandler 식별자의 함수 참조를 끊음
}
};
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) { // A
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function () { // B: fruit라는 외부 변수를 참조하므로 클로저가 있다.
alert('your choice is ' + fruit);
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
// B 함수의 쓰임새가 콜백 함수에 국한되지 않는 경우 반복을 줄이기 위해 외부로 분리
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); // [object MouseEvent] 출력
$li.addEventListener('click', alertFruit.bind(null, fruit));
// bind 활용하여 클로저가 발생하지 않지만 여러가지 제약사항 발생
// 이벤트 객체가 인자로 넘어오는 순서가 바뀌게 되고, 함수 내부에서의 this가 원래와 달라지게 된다.
$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);
});
...
// 클릭 이벤트가 발생하면 비로소 이 함수의 실행 컨텍스트가 열리면서 fruit 참조
5-3-2 접근 권한 제어 (정보 은닉)
- 정보 은닉 (information hiding) : 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높인다.
- 외부에 제공하고자 하는 정보들을 모아서 return하고, 내부에서만 사용할 정보들은 return하지 않는 것으로 접근 권한 제어가 가능
- 자동차 경주 게임
- 각 턴마다 주사위를 굴려 나온 숫자(km)만큼 이동한다.
- 차량별로 연료량(fuel)과 연비(power)는 무작위로 생성된다.
- 남은 연료가 이동할 거리에 필요한 연료보다 부족하면 이동하지 못하낟.
- 모든 유저가 이동할 수 없는 턴에 게임이 종료된다.
- 게임 종료 시점에 가장 멀리 이동해 있는 사람이 승리
var createCar = function () {
var fuel = Math.ceil(Math.random() * 10 + 10); // 연료(L) 10 ~ 20
var power = Math.ceil(Math.random() * 3 + 2); // 연비(km/L) 3 ~ 5
var moved = 0; // 총 이동거리
var publicMembers = {
get moved () {
return moved
},
run: function () {
var km = Math.ceil(Math.random() * 6); // 1 ~ 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) : 여러개의 인자를 받는 함수에서 미리 일부의 인자만 넘겨 기억시켰다가, 나중에 나머지 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수
// bind 메서드를 활용한 부분 적용 함수
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)); // 55
- 위의 add 함수는 this를 사용하지 않으므로 bind 메서드만으로도 구현 가능. 그러나 this의 값을 변경할 수밖에 없기 때문에 메서드에서는 사용할수 없다.
// 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)); // 55
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)); // 55
- 빈 자리에 _로 표시하여 비워놓은 공간마다 나중에 넘어온 인자들이 차례대로 끼워넣어진다.
- Symbol.for (ES6) :
Symbol.for()
함수는 전역 심볼 레지스트리 목록에서 사용 가능한 심볼을 생성합니다.Symbol.for()
는 호출할 때마다 새로운 심볼을 생성하지는 않으며 레지스트리에서 주어진key
를 갖는 심볼이 이미 존재하는지를 먼저 확인합니다. 존재하는 경우 해당하는 심볼이 반환됩니다. 주어진 키를 갖는 심볼이 존재하지 않는 경우,Symbol.for()
는 새로운 전역 심볼을 생성합니다. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Symbol/for
- 디바운스 : 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리. (scroll, wheel, mousemove, resize 등에 적용하기 좋다)
// 부분 적용 함수 - debounce
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); // wait 시간 뒤에 func 실행
// 그런데 wait 시간이 경과하기 전에 다시 동일한 event 발생하면
};
};
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)); // 10
console.log(getMaxWith10(25)); // 25
var getMinWith10 = curry3(Math.min)(10);
console.log(getMinwith10(8)); // 8
console.log(getMinwith10(25)); // 10
- 인자가 많아질수록 가독성이 떨어진다는 단점이 있다.
- 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);
};
};
};
// ES6
var getInformation = baseUrl => path => id => fetch(baseUrl + path + '/' + id);
var imageUrl = 'http://imageAddress.com/';
var getImage = getInformation(imageUrl);
var getEmoticon = getImage('emoticon'); // http://imageAddress.com/emoticon
var emoticon1 = getEmoticon(100); // http://imageAddress.com/emoticon/100
- Redux에서의 커링 사용
// Redux Middleware 'Logger'
const logger = 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);
};
- store, next, action 순서로 인자를 받는다. 이 중 store는 프로젝트 내에서 한 번 생성된 이후로는 바뀌지 않고, dispatch의 의미를 가지는 next도 마찬가지지만 action의 경우는 매번 달라진다.
- store, next를 미리 넘겨서 반환된 함수를 저장시켜놓고, 이후에는 action만 받아서 처리할 수 있게 되어있다.
Author And Source
이 문제에 관하여(Core Javascript 05 클로저), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@yhkim3116/Core-Javascript-05-클로저저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)