일시: 2022.11.27
참석자:
금주 스터디는 오예린님께서 발표해주셨습니다.
클로저
클로저의 의미 및 원리 이해
- 클로저란?
- 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료가 되더라도 변수 a가 사라지지 않는 현상
- 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료가 되더라도 변수 a가 사라지지 않는 현상
1) 외부 함수의 변수를 참조하는 내부 함수 - 내부 함수 자체를 return 하는 경우
```javascript
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 함수의 실행 컨텍스트가 종료가 되어도 outer2 변수는 outer의 실행 결과인 inner 함수 참조
- inner 함수가 참조된다면 outerEnvironmentReference는 inner 함수가 선언된 위치의 LexicalEnvironment가 참조복사됨, 즉, outer 함수의 LexicalEnvironment가 담김
- 정리하자면, outer에서 선언한 a에 접근하고, 1을 더한 값을 반환하며, inner 함수의 실행 컨텍스트를 종료함 <br> <br>
> <br>🎯 outer 함수의 실행 컨텍스트가 종료가 되었는데 outer 함수 내부 함수(inner)가 잘 실행되는 이유? <br><br> -> 가비지 컬렉터의 동작 방식 때문 <br> -> 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 수집을 하지 않는 특성을 가짐 <br> <br>
2) return 없이도 클로저가 발생하는 기타 경우
```javascript
(function () {
var a = 0;
var intervalId = null;
var inner = function() {
if (++a >= 10){
clearInterval(intervalId);
}
console.log(a);
};
intervalId = setInterval(inner, 1000)l
})();
```
- 별도 외부객체 window의 메서드(setTimeout or setInterval)에 전달한 콜백 함수 내부에서 지역변수 참조
클로저와 메모리 관리
- 클로저 사용을 할 경우 현재 자바스크립트 엔진에서 우리는 메모리 소모 만 잘 관리하면 됨
- 클로저를 사용을 한다 == 의도적으로 함수의 지역변수가 메모리를 소모하도록 유지함 == 불필요한 시점이 되면 메모리 소모를 하지 않도록 조치해주면 됨
- 참조 카운트를 0으로 만들어 가비지 컬렉터의 수집 대상이 되도록 변경하기
- 식별자에 참조형이 아닌 기본형 데이터 (대개 null이나 undefined)를 할당해주면 끝
var outer = function(){ var a = 1; var inner = function() { return ++a; } return inner; }; var outer2 = outer(); console.log(outer2()); console.oog(outer2()); outer = null; //outer 식별자에 inner 함수 참조를 끊어줌 == GC 수거대상이 되고, 메모리 소비를 줄일 수 있음
클로저 활용 사례
- 콜백 함수 내부에서 외부 데이터 사용할 때
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.vbody.appendChild($ul);
- fruits 변수를 순회하여 li 생성, li 클릭 시 리스너에 기억된 콜백 함수 실행
- $li.addEventListener('click', function() ... 이 부분에서 넘겨준 콜백 함수(B)는 외부 변수 fruits를 참조하고 있어 클로저 발생
- A 함수의 종료 여부와 관계 없이 B 함수가 실행될 때 B의 outerEnvironmentReference는 A의 변수 내용이 포함된 LexicalEnvironment 참조함 == B가 참조하는 변수 fruits는 A가 종료되어도 계속 참조할 것
(2) bind 메서드로 직접 값 넘겨주기- B 함수를 사용할 때 반복되는 점을 무마하기 위해 외부 함수로 빼고, bind 메서드를 활용해 값을 직접 넘겨주는 식으로 코드 작성 가능
- 이때, bind 메서드 활용 시 이벤트 객체가 인자로 넘어오는 순서가 바뀌고 함수 내부 this가 가리키는 대상이 달라짐
- 이를 해결하기 위해서는 고차함수를 활용하는 방식이 존재함
(3) 콜백 함수를 고차함수로 바꿔 클로저 활용하기- 고차함수란, 함수를 인자로 받거나 함수를 리턴하는 함수를 의미함
- 해당 예씨에서는 alertFruit 대신 alertFruitBuilder 함수 사용 > alertFruitBuilder가 alertFruit와 같은 한 익명함수를 리턴함
- 클릭 이벤트 발생 시 함수의 실행 컨텍스트가 열리면서 alertFruitBuilder의 인자로 넘어온 fruit를 outerEnvironmentReference에 의해 참조 가능! == alerFruitBuilder의 실행 결과로 반환된 함수에는 클로저 존재
... var fruits =['apple', 'banana', 'peach']; var $ul = document.createElement('ul'); 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)); //alertFruitBuilder 함수 실행 시 fruit 값을 인자로 전달. 실행 결과 다시 함수가 되고, 반환된 함수를 리스너에 콜백 함수로 전달. $ul.appendChild($li); }); ...
... var fruits =['apple', 'banana', 'peach']; var $ul = document.createElement('ul'); var alertFruit = function(fruit){ //B가 외부로 나왔다 alert('your choice is '+fruit); } fruits.forEach(function(fruit){ var $li = document.createElement('li'); $li.innerText = fruit; $li.addEventListener('click', alertFruit.bind(null, fruit)); $ul.appendChild($li); }); ...
- (1) 콜백 함수를 내부함수로 선언해 외부 변수를 직접 참조
- 접근 권한 제어할 때(정보 은닉 시)
- 자바스크립트의 경우 다른 언어와 달리 public, private, protected와 같은 접근 권한을 직접 부여하지는 않아도 됨
- BUT 클로저를 활용할 경우 public한 값과 private한 값을 구분짓는 것이 가능함
var outer = function(){ var a = 1; var inner = function() { return ++a; } return inner; //inner 함수 return함으로서 outer 함수의 지역변수 a를 외부에서 접근할 수 있게 함 //즉, 클로저를 잘 활용한다면 함수 내부의 변수들을 선택적으로 외부에 공개할 수 있고, 접근 권한을 부여할 수 있음 }; var outer2 = outer(); console.log(outer2()); console.oog(outer2());
- 부분 적용 함수
- 부분 적용 함수란?
- n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시킨 후, 나중에 (n-m)개의 인자를 넘기면 함수의 실행 결과를 얻을 수 있게끔 하는 함수
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)); //추가적으로 나머지 인자들을 받아 concat된 원본 함수 출력. 55를 출력 // 부분 적용 함수의 예시 var dog = { name: '강아지', greet: partial(function(prefix, suffix){ return prefix + this.name + suffix; }, '왈왈, ') } dog.greet('입니다!'); //왈왈, 강아지입니다.
- 미리 일부 인자를 넘겨주고 기억을 하게 한 다음, 추후 필요할 때 기억한 인자들까지 함께 실행한다는 개념 자체가 클로저의 정의에 부합
- 디바운스
- 짧은 시간 동안 반복적인 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 or 마지막에 발생한 이벤트에 대해서 한 번만 처리하는 방식
- 프론트엔드 성능 최적화에 큰 도움을 주는 기능 중 하나
- scroll, wheel, mousemove, resize에 적용하기 좋음
var debounce = function(eventName, func, wait){ var timeoutId = null; return function(event){ var self = this; //이벤트가 발생한 DOM 요소 console.log(eventName, 'event 발생'); clearTimeout(timeoutId); //B timeoutId = setTimeout(func.bind(self, event), wait); //A } } var moveHandler = function(e){ console.log('move event'); } var wheelHandler = function(e){ console.log('wheel event'); } //이벤트가 발생하면 debounce 함수가 실행이 됨 document.body.addEventListener('mousemove', debounce('move', moveHandler, 500)); document.body.addEventListener('mousewheel', debounce('wheel', wheelHandler, 700));
- 최초로 event가 발생한다면 timeout 대기열에 wait 시간 뒤에 func를 실행할 것이라는 내용 담김 (A)
- wait 시간이 경과하기 전 동일한 event가 발생한다면 앞서 저장했던 대기열 초기화 (B) 다시 새로운 대기열 등록 (A)
- 결론적으로 마지막으로 등록되어있는 대기열만 한 번 실행이 됨
- 부분 적용 함수란?
- 커링 함수
- 커링 함수란?
- 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠 순차적으로 호출이 되도록 체인 형태로 구성한 것을 의미
- 한 번에 하나의 인자만 전달하며, 마지막 인자가 전달되기 전까지는 원본 함수가 실행 X
var curry3 = function(func){ return function (a){ return function (b){ return func(a, b); }; }; }; var getMaxWith10 = curry3(Math.max)(10); //getMaxWith10 == function(b) { return Math.max(10, b) } console.log(getMaxWith10(8)); //10 console.log(getMaxWith10(25)); //25 //10과 숫자 인자 b를 한 번에 하나씩 전달을 해줬다는 점 & 마지막 인자 b가 전달이 되기 전까지는 우리가 원하는 //리턴 값이 전달이 되지 않았다는 점에서 이는 커링 함수라고 할 수 있음 var getMinWith10 = curry3(Math.min)(10); console.log(getMinWith10(8)); //8 console.log(getMinWith10(25)); //10
부분 적용 함수는 여러 개의 인자 전달 가능, 실행 결과 재실행 시 원본 함수가 실행이 된다는 점에서 차이 有)
-
- 인자가 많아질수록 가독성이 떨어진다는 점을 방지하기 위해서 화살표 함수를 활용함
- 함수에 어떤 값을 차례로 넘겨줄 것이고, 마지막에 호출될 함수(원본함수)가 무엇인지 한눈에 파악 가능
- 각 단계에서 받은 인자들은 모두 마지막에 참조를 하므로 GC 대상이 아님 == 문제 없이 실행됨
- 커링 함수가 유용한 경우
- 지연 실행(lazy execution) 프로그래밍 (마지막 인자가 넘어갈 때까지 함수 실행 미루는 것)이 필요할 때
- 프로젝트 내에서 자주 쓰이는 함수의 매개변수의 일부만 바뀔 때
'4-1기 스터디 > Javascript-Typescript Deep Dive' 카테고리의 다른 글
[JS Deep Dive 스터디] 7주차 - 클래스 (1) | 2023.01.26 |
---|---|
[JS Deep Dive 스터디] 6주차 - 프로토타입 (0) | 2023.01.25 |
[JS Deep Dive 스터디] 4주차 - 콜백 함수 (1) | 2023.01.25 |
[JS Deep Dive 스터디] 3주차 - this (0) | 2023.01.25 |
[JS Deep Dive 스터디] 2주차 - 실행 컨텍스트 (0) | 2022.11.06 |
댓글