Notice
Recent Posts
Recent Comments
Link
뮁이의 개발새발
[코어자바스크립트] Chapter 5. 클로저 본문
Chapter 5. 클로저
클로저란?
- 어떤 컨텍스트 A에서 선언한 내부함수 B의 실행 컨텍스트가 활성화된 시점에 B의 outerEnvironmentReference가 참조하는 대상인 A의 LexicalEnvironment에 접근이 가능하게 됨.
- 이때 A에서는 B에서 선언한 변수에 접근할 수 없지만 B에서는 A에서 선언한 변수에 접근이 가능함
- 내부함수 B가 A의 LexicalEnvironment를 언제나 사용하는것은 아님.
- 내부함수에서 외부 변수를 참조하는 경우에 한해서만 combination이 됨.
⇒ 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상
// 외부 함수의 변수를 참조하는 내부 함수(1)
var outer = function() {
var a = 1;
var inner = function() {
console.log(++a);
};
inner();
};
outer();
- outer함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들(a, inner)에 대한 참조를 지움 ⇒ Gabage Collecter 대상이 됨
// 외부 함수의 변수를 참조하는 내부 함수(2)
var outer = function() {
var a = 1;
var inner = function() {
return ++a; // 외부 함수의 변수를 참조 -> 클로저
};
return inner(); // 리턴하는 시점에 a를 참조하는 대상이 없어짐
};
var outer2 = outer();
console.log(outer2); // 2
- inner를 리턴하는 시점에 a와 inner 변수의 값들은 가비지 컬렉터에 의해 소멸됨
- 위 두 함수는 outer 함수의 실행 컨텍스트가 종료되기 이전에 inner 함수의 실행 컨텍스트가 종료돼 있으며, 이후 별도로 inner 함수를 호출할 수 없다는 공통점이 있음.
// 외부 함수의 변수를 참조하는 내부 함수(3)
var outer = function() {
var a = 1;
var inner = function() {
return ++a;
};
return inner; // inner의 결과값이 아닌 함수 자체를 반환
};
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
- inner 함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없음
- outerEnvironmentReference에는 inner함수가 선언된 위치의 LexicalEnvironment가 참조 복사됨. (outer 함수의 LexicalEnvironment가 담기게됨)
클로저의 정의 정리
- 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상
- ⇒ 외부 함수의 LexicalEnvironment가 가비지 컬렉팅되지 않는 현상
- 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상
- 여기서 외부로 전달한다는것은 콜백함수, 외부 객체 등 return외에도 포함될 수 있음
클로저와 메모리 관리
- 클로저로 인한 메모리 소모가 일부 존제하기 때문에 관리법을 잘 파악해서 적용해야 함.
- 클로저가 발생하는 이유?
- 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생
- 참조 카운트를 0으로 만들면 회수가 됨.
- 0으로 만드는 방법은 식별자에 참조형이 아닌 기본형 데이터(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) 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);
})();
클로저 활용 사례
콜백 함수 내부에서 외부 데이터를 사용하고자 할때
- 이벤트리스너를 통한 예제
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를 인자로 받아 출력하는 형태로
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
// 별도의 함수로 뺐음
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]);
- 위 코드는 각 li를 클릭하면 클릭한 대상의 과일명이 아닌 [object MouseEvent]라는 값이 출력됨.
- 콜백함수 인자에 대한 제어권을 addEventListener가 가지고 있으며, addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 ‘이벤트 객체’를 주입하기 때문
위를 해결한 코드
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
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.bind(null, fruit)); // bind를 통해 제어권을 alertFruit가 가지게끔 함.
$ul.appendChild($li);
});
document.body.appendChild($ul);
- 이벤트 객체가 인자로 넘어오는 순서가 바뀜
- 함수 내부에서의 this가 원래의 그것과 달라지는 점은 감안해야함
- 따라서 bind가 아닌 ‘고차함수’를 활용해야 함.
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
var alertFruitBuilder = function(fruit) {
return function() { // alertFruit를 반환 (함수형태, fruit로 인해 클로저가 존재
alert('your choice is ' + fruit);
};
};
fruits.forEach(function(fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruitBuilder(fruit)); // 해당 함수(alertFruitBuilder)의 실행 결과가 함수가 됨.
$ul.appendChild($li);
});
document.body.appendChild($ul);
접근 권한 제어(정보 은닉)
- 정보 은닉
- 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념중 하나
- public, private, protected
- public : 외부에서 접근 가능한 것
- private : 내부에서만 사용하며 외부에 노출되지 않는 것
- 클로저를 통해 이러한 값들을 구분하는것이 가능함
var outer = function() { var a = 1; var inner = function() { return ++a; }; return inner; }; var outer2 = outer(); console.log(outer2()); // 2 console.log(outer2()); // 3
- outer 함수를 종료할 때 inner 함수를 반환함으로써 outer 함수의 지역변수인 a를 외부에서도 읽을 수 있게됨.
- ⇒ 클로저를 활용하면 외부 스코프에서 함수 내부의 변수들 중 선택적으로 일부의 변수에 대한 접근 구너한을 부여할 수 있게 됨. (return을 활용)
- 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념중 하나
- 외부에 제공하고자 하는 정보들을 모아서 return(공개 멤버), 내부에 사용할 정보들은 return하지 않는 형태로 접근 권한 제어를 진행(비공개 멤버)
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, car.power 등등..)
- 객체가 아닌 함수로 만들고, 필요한 멤버만을 return할 수 있도록 제어해야함. (클로저로 보호)
var createCar = function() {
var fuel = Math.ceil(Math.random() * 10 + 10); // 연료(L)
var power = Math.ceil(Math.random() * 3 + 2); // 연비(km / L)
var moved = 0; // 총 이동거리
return {
get moved() {
return moved;
},
run: function() {
var km = Math.ceil(Math.random() * 6);
var wasteFuel = km / power;
if (fuel < wasteFuel) {
console.log('이동불가');
return;
}
fuel -= wasteFuel;
moved += km;
console.log(km + 'km 이동 (총 ' + moved + 'km). 남은 연료: ' + fuel);
},
};
};
var car = createCar();
- createCar라는 함수를 통해 객체를 생성함으로써, fuel, power 등에 대한 변수에 대한 접근이 불가능하게끔 함
- 여기서 비공개 멤버 / 공개 멤버는?
- 외부에서는 run과 moved 값을 확인하는 두가지 동작만 가능하게 됨.
- run을 다른 내용으로 덮어씌우는 어뷰징은 가능하긴 함.
- 이를 막기 위한 코드
var createCar = function() { var fuel = Math.ceil(Math.random() * 10 + 10); // 연료(L) var power = Math.ceil(Math.random() * 3 + 2); // 연비(km / L) var moved = 0; // 총 이동거리 var publicMembers = { // moved 보호 get moved() { return moved; }, run: function() { // run 보호 var km = Math.ceil(Math.random() * 6); var wasteFuel = km / power; if (fuel < wasteFuel) { console.log('이동불가'); return; } fuel -= wasteFuel; moved += km; console.log(km + 'km 이동 (총 ' + moved + 'km). 남은 연료: ' + fuel); }, }; Object.freeze(publicMembers); return publicMembers; }; var car = createCar();
클로저를 활용해 접근 권한을 제어하는 방법
- 함수에서 지역변수 및 내부함수 등을 생성
- 외부에 접근권한을 주고자 하는 대상들로 구서왼 참조형 데이터(대상이 여럿일 때는 객체 또는 배열, 하나일 때는 함수)를 return함
- return한 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 됨.
부분 적용 함수
- 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); // bind를 사용해야 함.
console.log(addPartial(6, 7, 8, 9, 10)); // 55
- this의 값을 변경할 수 밖에 없는 상황
// 부분 적용 함수 구현 (1)
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)); // concat 사용
};
};
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('입니다!'); // 왈왈, 강아지입니다.
- concat을 이용하여 반환할 함수들의 인자를 모아서 원본함수를 호출함. this에는 영향을 주지 않음
- 다만, 반드시 앞에서부터 차례대로 전달할 수 밖에 없음
// 부분적용 함수 구현(2)
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)); // 55
var dog = {
name: '강아지',
greet: partial2(function(prefix, suffix) {
return prefix + this.name + suffix;
}, '왈왈, '),
};
dog.greet(' 배고파요!'); // 왈왈, 강아지 배고파요!
- 미리 실행할 함수의 모든 인자 개수를 맞춰 빈 공간을 확보하지 않아도 됨.
- 최종 실행 시, 인자 개수가 많든 적든 잘 실행됨
⇒ 위 두 코드 모두 클로저를 핵심 기법으로 활용
미리 일부 인자를 넘겨두어 기억하게끔 하고 추후 필요한 시점에 기억했던 인자들까지 함께 실행하게 함.
디바운스
- 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것
- 성능 최적화에 큰 도움을 줌. (scroll, wheel, mousemove, resize 등)
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);
};
};
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)
);
- wait 시간이 경과하기 이전에 동일한 event가 발생하면, 앞서 저장했던 대기열을 초기화하고 다시 새로운 대기열을 등록하게 됨.
- 마지막에 발생한 이벤트만이 초기화되지 않고 무사히 실행될 것
- eventName, func, wait, timeoutId 등
- [참고] 전역 공간에 대한 침범을 안하기 위해 “Symbol.for”라는 메서드를 활용하기도 함.
- 어디서든 접근 가능하면서 유일무의한 상수를 맞들고자 할 때 적합함
커링 함수
- 여러개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것
- 한 번에 하나의 인자만 전달하는 것을 원칙으로 함
- 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않음.
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);
- 당장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 수행. 마지막 인자가 넘어갈 때까지 함수 실행을 미룰 수 있음 → 지연 실행
- 공통적인 요소는 먼저 기억시켜두고 특정한 값(id)만으로 서버 요청을 수행하는 함수
정리
- 클로저란?
- 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상
- 내부함수를 외부로 전달하는 방법?
- 함수를 return하는 경우, 콜백으로 전달하는 경우
- 클로저는 메모리를 계속 차지하는 개념
- 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 않도록 관리해줄 필요가 있음
'Front-end > Javascript' 카테고리의 다른 글
[코어자바스크립트] Chapter 4. 콜백함수 (0) | 2024.05.18 |
---|---|
[코어자바스크립트] Chapter 3. this (1) | 2024.04.10 |
[코어 자바스크립트] Chapter 2. 실행 컨텍스트 (0) | 2024.04.08 |
[코어 자바스크립트] Chapter 1. 데이터 타입 (1) | 2024.04.08 |
[Javascript] jQuery Event (0) | 2021.09.06 |
Comments