var outer = function(){
var a=1;
var inner = function(){
return ++a;
};
return inner;
}
var outer2 = outer();
console.log(outer2()); //2
console.log(outer2()); //3
위와 같은 코드 처럼 inner함수의 실행시점에는 outer함수는 이미 실행이 종료된 상태인데 outer함수의 lexicalEnvironment에 접근할수 있는 이유는 가비지 컬렉터의 동작 방식 때문입니다. 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않습니다. 위의 코드를 예시로 들면 outer함수는 실행 종료 시점에 inner함수를 반환합니다. 외부함수인 outer의 실행이 종료되더라도 내부 함수인 inner함수는 언젠가 outer2를 실행함으로써 호출될 가능성이 열린 것.
클로저란?
❗️위의 설명을 예시로 들었는데 클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상을 말한다.
여기서 하나 주의해야 할 점은 외부로 전달이 곧 return만을 의미하는 것은 아니라는 점return 없이도 클로저가 발생하는 다양한 경우
//(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 (function(){ var count=0; var button = document.createElement('button'); button.innerText ='click'; button.addEventListener('click',function(){ console.log(++count, 'times clicked'); }); document.body.appendChild(button); })();
(1)은 별도의 외부객체인 window의 메서드(setTimeout 또는 setInterval)에 전달할 콜백 함수 내부에서 지역변수를 참조합니다. (2)는 별도의 외부객체인 DOM의 메서드 (addEventListener)에 등록할 handler 함수 내부에서 지역변수를 참조합니다. 두 상황 모두 지역변수를 참조하는 내부함수를 외부에 전달했기 때문에 클로저.
이렇게 가비지 컬렉팅이 일어나지 않아 메모리를 계속 잡아먹고 있을 경우 메모리 누수가 발생할 수 있음
해결 방법: 식별자에 null 이나 undefined를 할당
//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 함수 참조를 끊음 console.log(outer());//error
//setInterval에 의한 클로저의 메모리 해제 var func= function(){ var a=0; var intervalId=null; var innerFunc=function(){ if(++a>=10){ clearInterval(intervalId); innerFunc=null; //innerFunc 식별자의 함수 참조를 끊음 } console.log(a); }; intervalId=setInterval(innerFunc,1000); }; func();
//(3)eventListener에 의한 클로저의 메모리 해제 (function(){ var count=0; var button= document.createElement('button'); button.innerText='click'; var clickHandler = function(){ console.log(++count, 'time clicked'); if(count>=10){ button.removeEventListener('click', clickHandler); clickHandler=null; //clickHandler 식별자의 함수 참조를 끊음 } }; button.addEventListener('click', clickHandler); document.body.appendChild(button); })();
클로저 활용 사례
콜백 함수 내부에서 외부 데이터를 사용하고자 할때
//예시 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 choic is ' +fruit); }); $ul.appendChild($li); }); document.body.appendChild($ul);
forEach 메서드에 넘겨준 익명의 콜백 함수 A는 그 내부에서 외부 변수를 사용하지 않고 있으므로 클로저가 없지만 addEventListener에 넘겨준 콜백 함수 B에는 fruit이라는 외부 변수를 참조하고 있으므로 클로저가 있다.
//좀 더 개선한 코드(1) var fruits =['apple', 'banana', 'peach']; var $ul= document.createElement('ul'); var alertFruit = function(fruit){ alert('your choic 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]);
❗️저렇게 bind를 붙여주는 이유는 콜백 함수의 인자에 대한 제어권을 addEventListener가 가진 상태이며, addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 이벤트 객체를 주입하기 때문
//좀 더 개선한 코드(2) 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); }); document.body.appendChild($ul); alertFruit(fruits[0]);
위의 코드에서는 alertFruit함수 대신 alertFruitBuilder라는 이름의 함수를 작성함으로써 함수 내부에서 익명 함수를 다시 반환한다. 그리고 alerFruitBuilder 함수를 실행함으로써 fruit값을 인자로 전달한다. 그러면 이 함수의 실행 결과가 다시 함수가 되며, 이렇게 반환된 함수를 이벤트 리스너에 콜백 함수로써 전달하는 것
접근 권한 정보(정보 은닉)
정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나 이다.
❗️자바스크립트는 기본적으로 변수 자체에 접근 권한을 직접 부여하도록 설계돼 있지 않아 클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하는 것이 가능
❗️외부에 제공하고자 하는 정보들을 모아서 return하고, 내부에서만 사용할 정보들은 return하지 않는 것으로 접근 권한 제어가 가능한 것. return한 변수들은 공개멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 되는 것.
/* 자동차 게임 1.) 각턴마다 주사위를 굴려 나온 숫자(km)만큼 이동한다. 2.) 차량별로 연료량(fuel)과 연비(power)는 무작위로 생성된다. 3.) 남은 연료가 이동할 거리에 필요한 연료보다 부족하면 이동하지 못한다. 4.) 모든 유저가 이동할 수 없는 턴에 게임이 종료된다. 5.) 게임 종료 시점에 가장 멀리 이동해 있는 사람이 승리 */ //간단한 자동차 객체 let car={ fuel: Math.ceil(Math.random()*10+10), power: Math.ceil(Math.random()*3+2), moved:0, run: function(){ var km= Math.ceil(Math.random()*6); var wastedFuel = km / this.power; if(this.fuel<wastedFuel){ console.log("이동 불가"); return; } this.fuel -=wastedFuel; this.moved +=km; console.log(km+'km 이동( 총' + this.moved+'km) 남은 연료:'+this.fuel); } };
위의 코드에 문제점은 사용자가 직점 연료, 연비, 이동거리를 바꿀수 있다.
car.fuel=1000; car.power=100; car.moved=1000;
❗️이와 같은 문제점의 해결책은 클로저를 활용하는것이다. 즉 객체가 아닌 함수로 만들고, 필요한 멤버만을 return 하는 것.
var createCar = function(){ var fuel = Math.ceil(Math.random()*10+10)//연료(L) var power =Math.ceil(Math.random()*+2)//연비(km/L) var moved = 0; return{ get moved(){ return moved; }, run: function(){ var km = Math.ceil(Math.random()*6); var wastedFuel = km/power; if(fuel <wastedFuel){ console.log('이동 불가'); return; } fuel -= wastedFuel; moved +=km; console.log(km+'km 이동( 총'+moved+'km) 남은 연료: '+fuel); } }; };
비록 run 메서드를 다른 내용으로 덮어씌우는 어뷰징은 여전히 사능한 상태이긴 하지만 앞서의 코드보다는 훨씬 안전한 코드가 됌. 이런 어뷰징까지 막기 위해서는 객체를 return하기 전에 미리 변경할 수 없게끔 조치를 취해야 한다.
var createCar =function(){ var fuel = 0;//연료(L) var power = Math.ceil(Math.random()*3+2)//연비(km/L); var moved = 0; var publicMembers={ get moved(){ return moved; }, run: function(){ var km = Math.ceil(Math.random()*6); var wastedFuel = km/power; if(fuel<wastedFuel){ console.log('이동 불가'); return; } fuel -= wastedFuel; moved +=km; console.log(km+'km 이동( 총'+moved+'km) 남은 연료:'+fuel); } }; Object.freeze(publicMembers); return publicMembers; } var car= createCar();
부분 적용 함수
부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다.
//부분 적용 함수(1) 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));
addParial 함수는 인자 5개를 미리 적용하고, 추후 추가적으로 인자들을 전달하면 모든 인자를 모아 원래의 함수가 실행되는 부분 적용 함수 이다.
//부분 적용 함수(2) 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; }, '왈왈') }; var test=dog.greet('입니다!'); console.log(test); //왈왈 강아지 입니다!
첫 번째 인자에는 원본 함수를, 두 번째 인자 이후부터는 미리 적용할 인자들을 전달하고, 반환할 함수(부분 적용 함수)에서는 다시 나머지 인자들을 받아 이들을 한데 모아 (concat) 원본 함수를 호출(apply)합니다. 또한 실행 시점의 this를 그대로 반영함으로써 this에는 아무런 영향을 주지 않게 됐습니다.
하지면 여기서의 아쉬운점은 함수에 넘길 인자를 반드시 앞에서부터 차례로 전달할 수 밖에 없다는 것이다.//인자들을 원하는 위치에 넣는 코드 Object.defineProperty(window,'_',{ value: 'EMPTY_SPACE', writable: false, configurable: false, enumerable: false }); 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] ===_){ 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 addPartial = partial2(add,1,2,_,4,5,_,_,8,9); console.log(addPartial(3,6,7,10)); var dog= { name: '강아지', greet: partial2(function(prefix,suffix){ return prefix +this.name +suffix; }, '왈왈') }; console.log(dog.greet('배고파요!'));
위의 코드와 같이 처음에 넘겨준 인자들중 _로 비워놓은 공간마다 나중에 넘어온 인자들이 차례대로 끼워놓도록 구현하였음.
디바운스
디바운스란 실무에서 부분 함수를 사용하기에 적합한 예로 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것으로 프로트엔드 성능 최적화에 큰 도움을 주는 기능 중 하나이다.(scroll, wheel, mousemove, resize 등에 적용하기 좋다.)
커링 함수
커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말한다. 커링은 한번에 하나의 인자만 전달하는 것을 원칙으로 합니다. 또한 중간 과정상의 함수를 실행한 결과는 그다음 인자를 받기 위해 대기할 뿐으로, 마지막 인자가 전달되기 전까지는 원본 함수가 실행 되지 않습니다.(❗️부분 적용 함수는 여러 개의 인자를 전달할 수 있고, 실행 결과를 재실행할때 원본 함수가 무고전 실행됩니다.)
//커링 함수(1) 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)(5); console.log(getMinWith10(2)); //2출력 console.log(getMinWith10(60)); //5출력
//가독성이 떨어지는 커링 함수 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 getMaxWith10 = curry5(Math.max); console.log(getMaxWith10(1)(2)(3)(4)(5)); //5출력
//화살표 함수를 이용한 커링 함수 var curry6=func=>a=>b=>c=>d=>e=>f=>func(a,b,c,d,e,f); var getMaxWith10 = curry5(Math.max); console.log(getMaxWith10(1)(2)(3)(4)(5)(2)); //5출력
❗️이 커링 함수가 유용한 경우가 있습니다. 당장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 하면 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 셈이 됩니다. 이를 함수형 프로그래밍에서는 지연실행이라고 칭합니다.