클로저의 의미 및 원리 이해
- 클로저란?
: 어떤 함수 A에서 선언한 변수 a를 참조하는 내부 함수 B를 외부로 전달할 경우,
A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상
// 외부 함수의 변수를 참조하는 내부 함수(1)
var outer = functionl () {
var a = 1;
var inner = function() {
//a 선언 X => 스코프 체이닝
// environmentRecord에서 값 못 찾음 => outerEnvironmentReference에 지정된 상위 컨텍스트 outer의 LE 접근 (a 찾음)
console.log(++a);
};
inner();
};
outer();
//함수 실행 종료 => LE 저장된 식별자(a, inner)에 대한 참조 지움
// => GC(Garbage Collector) 수집
//외부 함수의 변수를 참조하는 내부 함수(2)
var outer = function() {
var a = 1;
var inner = function() {
return ++a;
};
return inner(); // 함수 실행 결과 반환
};
var outer2 = outer();
console.log(outer2); // 2
var outer = function() {
var a = 1;
var inner = function() {
return ++a;
};
return inner; // 함수 자체 반환
};
var outer2 = outer();
console.log(outer2()); //2
console.log(outer2()); // 3
- 가비지 컬렉터
: 어떤 값을 참조하는 변수가 하나라도 있을 경우 해당 값은 수집 대상 포함 X
- 외부로 전달이란?: 외부로 전달 === return (X)
// (1) setInterval/setTimeout (function () { var a = 0; var intervalId = null; var inner = function() { if(++a >= 10) { clearInterval(intervalId); } console.log(a); }; intervalId = setInterval(inner, 1000); // 지역 변수 참조 })(); //(2) eventListener (funtion () { var count = 0; var button = document.createElement('button'); button.innerText = 'click'; button.addEventListener('click', function() { // handler 함수 내에서 지역변수 참조 console.log(++count, 'times clicked'); }); document.body.appendchild(button); })();
클로저의 메모리 관리
- 메모리 누수?
: 개발자의 의도와 달리 어떤 값의 참조 카운트가 0이되지 않아 GC의 수거 대상 X
- 메모리 관리
: 어떤 필요에 의해 의도적으로 함수의 지역 변수를 메모리를 소모하도록 하여 클로저 발생
⇒ 필요성 없을 경우 참조 카운트 0으로 설정
⇒ 식별자에 기본형 데이터(ex. null, undefined) 할당
//(1) return에 의한 클로저의 메모리 해제
var outer = (function() {
var a = 1;
var inner = function() {
return ++a;
};
return inner;
})();
console.log(outer());
console.log(outer());
outer = null; // outer 식별자의 inner 함수 참조 끊음
//(2) setInterval에 의한 클로저의 메모리 해제
(function () {
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) evnetListener에 의한 클로저의 메모리 해제
(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);
documnet.body.appendChild(button);
})();
클로저 활용 사례
- 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때
(1) 콜백 함수를 내부함수로 선언해 외부 변수를 직접 참조
var fruits = ['apple', 'banana','peach'];
var $ul = document.createElement('ul'); // (공통 코드)
frutis.forEach(function (fruit)) { //A 익명 콜백 함수(클로저 X)
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function() { //(B)
alert('your choice is '+ fruit); // 외부 변수 : fruit(클로저 O_
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
//(B)의 outerEnvrionmentReference가 (A)의 LexicalEnvrionment 참조
//(B)를 외부로 분리(fruit을 인자로 받아 출려)
var alerFruit = 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 출력
// 콜백 함수 이자의 제어권 addEventListener가 가짐
// addEventListener는 콜백 함수 호출 시 첫번째 인자에 이벤트 객체 주입
$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
(2) bind 메서드 활용
//bind 메서드 활용
fruits.forEach(function(fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit.bind(null, fruit));
$ul.appendChild($li);
});
//이벤트 객체가 인자로 넘어오며 순서 바뀜
//함수 내부에서의 this를 원래의 지정된 상태로 유지 어려움
//(bind 메서드의 첫번째 인자가 새로 바인딩할 this값 => 이를 생략 불가)
(3) 고차함수 활용
//고차 함수* 활용
var alertFruitBuilder = function(fruit) {
return function() {
alert('your choice is ' + fruit);
};
};
fruits.forEach(function(fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruitBuilder(fruit));
$ul.appendChild($li);
});
- 정보 권한 제어(정보 은닉)
: 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화
⇒ 모듈간의 결합도 낮추고 유연성 높임
- 접근 권한
- public : 외부에서 접근 가능
- private : 내부에서만 사용 가능
- protected
- return을 활용해 일부 변수의 접근 권한 부여 가능
var outer = function(){
var a = 1;
var inner = function() {
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());
//outer 함수 종료 => inner 함수 반환 => outer의 지역변수인 a 노출
- 재밌는 자동차 놀이
각 턴마다 주사위를 굴려 나온 숫자(km)만큼 이동
차량별로 연료량(fuel)과 연비(power)는 무작위로 생성
남은 연료가 이동할 거리에 필요한 연료보다 부족하면 이동 X
모든 유저가 이동할 수 없는 턴에 게임이 종료
게임 종료 시점에 갖아 멀리 이동해 있는 사람이 승리
var car = {
fuel : Math.ceil(Math.random() * 10 + 10), //연료(L)
power : Math.ceil(Math.random() * 3 + 2), //연비(km/L)
moved : 0, //총 이동거리
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');
}
};
car fuel =10000;
car.power = 100;
car.moved = 1000;
//다음과 같이 값 설정 가능
클로저로 변수를 보호한 자동차 객체
car.run(); //3km 이동(총 3km). 남은 연료 : 17.4
console.log(car.moved); //3
console.log(car.fuel); //undefined
console.log(car.power); //undefined
car.fuel = 1000;
console.log(car.fuel); //1000 => 어뷰징은 가능(다르내용으로 덮어씌우기)
car.run(); //1km 이동(총 4km). 남은 연료 : 17.2
car.power= 100;
console.log(car.power); //100 => 어뷰징은 가능(다르내용으로 덮어씌우기)
car.run(); //4km 이동(총 8km). 남은 연료 : 16.4
car.moved= 1000;
console.log(car.moved); //8
car.run(); //2km 이동(총 10km). 남은 연료 : 16
어뷰징을 차단하기
var createCar = function() {
```
var publicMembers = {
'''
};
Object.freeze(publicMembers);
return publicMembers;
};
- 부분 함수 적용
: n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억, 추후 (n-m)개의 인자 넘길 시
⇒ 원래 함수의 실행 결과 얻음
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
부분 적용 함수(1)
: this에 관여하지 않는
var partial = function() {
var originalPartialArgs = arguments;
var func = originalPartialArgs[0];
if(typeof func !== 'function') {
throw new Error('첫번째 인자가 함수가 아닙니다');
}
return function () {
//외부 함수의 arguments를 빼고 두번째 인자부터 배열로 만듬
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
// partialArgs == [1, 2, 3, 4, 5] => 첫번째 인자 배열
//slice() : 어떤 배열의 begin부터 end-1에 대한 얕은 복사본을 새로운 배열 객체로 반환
//인자 1개 : 해당 인덱스부터 배열 객체 반환
//인자 2개 : 시작값, 마지막 값(인덱스 값 -1)
var restArgs = Array.prototype.slice.call(arguments); // 현재 함수의 arguments
// restArgs == [ 6, 7, 8, 9, 10 ] => 두번째 인자 배열
console.log(restArgs);
return func.apply(this, partialArgs.concat(restArgs)); //인자를 합쳐서 func 실행
//concat : 나머지 인자들을 받아 모음
//apply : 여러개의 인자를 하나의 배열로 보보내 원본 함수 호출
//apply()는 인수 배열 하나 call()은 인수 목록 받음ㄴ
//func 자체에서는 배열 자체 X 배열 속 원소들을 인자로 받음
};
};
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('입니다!');
- 인자들을 차례로 전달만 가능
부분 적용 함수 (2)
: 인자를 원하는 위치에 미리 넣은 후 빈 자리에 채워 실행하는
Object.defineProperty(window, '_', {
//window 객체에 _속성을 만들어 값을 넣고, 속성 설정
value: 'EMPTY_SPACE',
writable: false, //재할당 불가 (읽기 전용)
configurable: false, //삭제, 내부 속성 재정의 불가
enumerable: false //열거 불가
});
var partial12 = functino(){
var originalPartialArgs = arguments;
var 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);
//_(비어있는 칸)에 restArgs 인자들 차례대로 끼워 넣기
for (var i = 0; i < partialArgs.length; i++){
if(partialArgs[i] === _) {
partialArgs[i] = restArgs.shift();
}
}
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 addPartial2(add, 1,2,_,4,5,_,_,8,9);
console.log(addPartial(3,6,7,10)); //55
- Symbol.for 활용
- 전역 심볼 공간에 인자로 넘어온 문자열 있을 시 해당 값 참조, 없을 시 새로 생성
- 어디서든 접근 가능하며 유일무이한 상수 생성에 적합
부분 적용 함수(3)
- 디바운스
: 짧은 시간 동안 동일한 이벤트가 많이 발생
⇒ 처음 또는 마지막에 발생한 이벤트 한번만 처리
⇒ scroll, wheel, mousemove, resize 등 적용 가능
var debounce = function(eventName, func, wait){
var timeoutId = null;
return function (event) { //이벤트 핸들러
const self = this; //이벤트 발생한 DOM요소(body)
console.log(eventName, 'event 발생');
clearTimeout(timeoutId);
timeoutId = setTimeout(func.bind(self, event), wait);
//bind해주지 않을 시, setTimeout의 객체인 window가 this로 바인딩
};
};
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));
- 커링함수
: 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠 순차적으로 호출될 수 있게 체인 형태로 구성한 것
| 부분 적용 함수 | 커링 함수 |
| 여러 개의 인자를 전달 가능 | 한 번에 하나의 인자만 전달이 원칙 |
| 실행 결과를 재실행 시 원본 함수 무조건 실행 | 중간 과정상의 함수 실행 결과 ⇒ 다음 인자를 위해 대기 |
| 마지막 인자가 전달되기 전까지 원본 함수 실행 X |
var 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);
};
};
};
};
};
};
var getMax = curry5(Math.max);
console.log(getMax(1)(11)(7)(3)(5));//11
- 인자가 많아질수록 가독성 떨어짐
⇒ 화살표 함수 활용
var curry5_ = func => a => b => c => d =>e => func(a,b,c,d,e);
*고차함수
: 함수를 인자로 받거나 함수를 리턴하는 함수