개발 공부/JavaScript

클로저

무우너대갈 2022. 7. 16. 12:32
반응형

클로저의 의미 및 원리 이해

  • 클로저란?

       : 어떤 함수 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);

 

 

*고차함수

: 함수를 인자로 받거나 함수를 리턴하는 함수

반응형

'개발 공부 > JavaScript' 카테고리의 다른 글

프로토타입  (0) 2022.07.19
this 퀴즈  (0) 2022.07.17
콜백  (0) 2022.07.15
this  (0) 2022.07.14
실행 컨텍스트  (0) 2022.07.13