클로저는 JS고유 개념이 아닌 함수형 언어의 보편적 특성이다. 여러 문헌에서 클로저를 다르게 설명한다.
이 책에서 설명하는 클로저는 아래와 같다.
💡 클로저
어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상.
왜 그러한지 예시 코드를 통해 그 이유를 알아보자.
// 외부함수의 변수를 참조하는 내부함수(1)
var outer = function() {
var a = 1;
var inner = function() {
console.log(++a)
};
inner();
};
outer(); //2
2를 출력하고 함수의 실행 컨텍스트가 종료되면 LE에 저장된 식별자들의 참조를 지운다.
그럼 값들은 참조하는 변수가 없어서 가비지 컬렉터의 수집 대상이 된다.
// 외부함수의 변수를 참조하는 내부함수(2)
var outer = function() {
var a = 1;
var inner = function() {
return ++a;
};
return inner();
};
var outer2 = outer();
console.log(outer2); //2
이번 코드는 inner함수의 실행결과를 반환했다. 이는 outer함수 실행 컨텍스트가 종료되면 a변수를 참조하는 대상이 사라진다. (1)
과 같은 결과다.
(1)
, (2)
는 outer 실행 컨텍스트 종료 전 inner 실행 컨텍스트가 종료됐고 이후에 inner를 호출할 수 없다.
그럼 outer 실행 컨텍스트 종료 후 inner함수를 호출할 수 있게 해보자
// 외부함수의 변수를 참조하는 내부함수(3)
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 실행 컨텍스트 종료 시 outer2 변수는 outer 실행 결과인 inner 함수를 참조한다. 따라서 outer2를 호출하면 inner가 실행된다.
가비지 컬렉터는 특정 값을 참조하는 변수가 있다면 그 값을 수집 대상에서 제외한다. 그래서 inner 실행 시 outer 실행 컨텍스트는 종료됐음에도 outer의 LE에 접근할 수 있다.
return
말고도 window의 메서드, DOM의 메서드도 외부로 전달이 가능하다.
// setInterval/ setTimeout
(function() {
var a = 0;
var intervalId = null;
var inner = function() {
if(++a>=10) clearInterval(intervalId)
console.log(a);
}
intervalId = setInterval(inner, 1000);
})();
// eventListener
(function() {
var a = 0;
var button = document.creatElement('button');
button.addEventListener('click', function(){
console.log(++a);
});
document.body.appendChild(button);
})();
두 경우, 지역변수를 참조하는 내부함수를 외부로 전달했기에 클로저다.
관리방법
참조 카운트를 0으로 만드는 방법
식별자에 참조형이 아닌 기본형 데이터를 할당한다. (보통 null
, undefined
)
// return에 의한 클로저의 메모리 해제
var outer = function() {
var a = 1;
var inner = function() {
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2()); //2
console.log(outer2()); //3
outer2 = null; // outer2 식별자의 inner함수 참조를 끊음
// 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);
})();
// eventListener에 의한 클로저의 메모리 해제
(function() {
var a = 0;
var button = document.creatElement('button');
var clickHandler = function(){
console.log(++a);
if(a>=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){ // (A)
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function () { // (B)
alert('your choice is '+ fruit);
});
$ul.appendChild($li);
})
document.body.appendChild($ul);
콜백함수 B에는 외부 변수(fruit)를 참조하고 있어 클로저가 있다.
B함수가 참조할 변수 fruit은 A가 종료된 후 GC대상에 제외되어 계속 참조가 가능할 것이다.
허나 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);
$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
콜백 함수를 꺼내 alertFruit변수에 담으면 직접 실행할 수 있다. 마지막 줄에서 banana가 잘 출력된다.
하지만 click 시에는 [object MouseEvent]
라는 값이 출력된다.
왜냐하면
...
fruits.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit.bind(null, fruit));
$li.appendChild($li);
});
...
하지만 bind를 사용하면 몇가지 변경사항이 있다.
이러한 변경사항과 이슈를 해결하려면 고차함수를 이용해야 한다.
고차함수: 함수를 인자로 받거나 함수를 반환하는 함수
...
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));
$li.appendChild($li);
});
...
위처럼 고차함수를 이용하면 click시 함수의 실행 컨텍스트가 열리면서 인자로 넘어온 fruit을 outerEnvironmentReference에 의해 참조한다.
따라서 alertFruitBuilder함수에서 반환된 함수에는 클로저가 있다.
💡 정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념중 하나이다.
접근권한 종류
JS는 기본적으로 변수에 접근 권한을 부여할 수 없다.
하지만 return을 활용하면 외부 스코프에서 함수 내부 변수 중 일부 변수에 대한 접근 권한을 부여할 수 있다.
클로저를 이용한 접근권한 제어 방법
return
한다.제어를 해도 메서드를 덮어씌우는 어뷰징이 가능하다. 따라서 return전 조치해야 한다.
var creatCar = function(){
...
var publicMembers = {...};
...
Object.freeze(publicMembers);
return publicMembers;
};
Object.freeze()
메서드는 객체를 동결시켜 수정할 수 없다.
💡 부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억 시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다.
var add = function () {
var result = 0;
for (var i = 0l i < arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = add.bind(null, 1, 2, 3, 4, 5);
console.log(addPartial(5, 6, 7, 8, 9, 10)); // 55
이 함수는 this를 사용하지 않아 bind 만으로도 문제 없이 구현됐다. 하지만 this를 변경할 수 없어 메서드에서 사용할 수 없다.
디바운스는 짧은 시간 같은 이벤트가 다량 발생할 때 전부 처리하지 않고 처음 혹은 마지막에 발생한 이벤트만 처리한다.
성능 최적화에 도움을 주는 기능으로 scroll
, wheel
, mousemove
, resize
등에 적용하기 좋다.
var debounce = (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 = (e) => {
console.log('move event 처리');
};
var wheelHandler = (e) => {
console.log('wheel evnet 처리');
}
document.body.addEventListener('mousemove',debounce('move', moveHandler, 500));
document.body.addEventListener('mousewheel',debounce('wheel', moveHandler, 700));
이벤트가 이전 이벤트로부터 wait 시간 이내에 발생하는 한 마지막에 발생한 이벤트만이 초기화되지 않고 실행 될 것이다.
위 디바운스 함수에서 클로저로 처리되는 변수 eventName
, func
, wait
, timeoutId
💡 커링함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말한다.
부분 적용 함수와의 차이점
//커링 함수 예제
var curry3 = function (func){
return function (a){
return function (b){
return func(a,b);
};
};
};
var getMaxWhith10 = curry3(Math.max)(10);
console.log(getMaxWhith10(8)); //10
console.log(getMaxWhith10(20)); //20
커링 함수는 필요한 상황에 만들어 쓰기 편하다.
인자 개수 만큼 함수를 만들어 계속 반환하다 마지막에 다 조합한 최종 결과를 반환하면 된다.
인자가 많을수록 가독성이 떨어진다. 하지만 ES6의 화살표 함수를 이용하면 한줄로 작성할 수 있다.
단순 간결화를 넘어 가독성도 향상된다.
var curry3 = func => a => b => func(a,b);
인자들은 쌓였다가 마지막 호출로 실행 컨텍스트가 종료되고 전부 GC의 수거 대상이 된다.
💡 지연실행: 필요한 정보만 받아 전달하고 필요한 정보가 들어오면 전달하는 식으로 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 것.
커링 함수는 이 지연실행에 유용하다. 아래는 커링 함수가 적합한 경우이다.