- 클로저 : 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
- 즉, 컨텍스트 A에서 선언한 변수를 내부함수B에서 참조할 경우에 발생하는 현상
- 클로저의 이점 : 함수 종료 후에도 사라지지 않는 지역변수를 만들 수 있다.
외부 함수의 변수를 참조하는 내부 함수(1) )
var outer = function () { // 외부 함수
var a = 1;
var inner = function () { // 내부 함수
console.log(++a); // 2
};
inner();
};
outer();
- outer의 내부 함수인 inner함수에서 a의 값을 1만큼 증가시킨 다음 출력한다.
- outer 함수의 실행 컨텍스트가 종료되기 이전에 inner함수의 실행 컨텍스트가 종료돼 있으며, 이후 별도로 inner 함수를 호출할 수 없다.
외부 함수의 변수를 참조하는 내부 함수(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
- ① inner함수의 실행 결과가 아닌 inner함수 자체를 반환했다. 그러면 outer 함수의 실행 컨텍스트가 종료될 때(②) outer2 변수는 outer의 실행 결과인 inner함수를 참조하게 된다.
- 이후 ③에 outer2를 호출하면 앞서 반환된 함수인 inner가 실행된다.
클로저 발생 시의 콜스택 흐름 )
- a를 참조하는 변수가 하나도 없게 되면 가비지 컬렉터(GC)의 수집 대상이 되어 삭제된다. 하지만 호출될 가능성이 있는 경우에는 수집 대상에서 제외됨
- 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다.
- 클로저란 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수
클로저 다른 예시)
function user(_name) {
let _logged = true;
return {
get name() {return _name},
set name(v) {_name = v},
login() {_logged = true},
logout() {_logged = false},
get status() {
return _logged
? 'login'
: 'logout';
},
}
}
let roy = user('재남')
console.log(roy.name) // 재남
roy.name = '제이';
console.log(roy.name) // 제이
roy._name = '로이'
console.log(roy.name) // 제이
console.log(roy._name) // 로이
console.log(roy.status) // login
- 메모리 누수 : 개발자의 의도와 달리 어떤 값의 참조 카운트가 0이 되지 않아 Garbage Collector의 수거 대상이 되지 않는 경우 이다
클로저의 메모리 관리 )
// 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()); // Uncaught TypeError: outer is not a function
// 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);
})()
//1
//2
//3
..
//10
- 1초에 한번씩 전위식인 a 변수를 출력함
이벤트 리스너에 관한 예시 - 콜백함수와 클로저(1) )
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
ⓐ fruits.forEach(function (fruit){ // 클로저 x
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function (){
ⓑ alert('your choice is ' + fruit); // 클로저 o
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
결과 )
- fruits 변수를 순회하며 li를 생성하고, 각 li를 클릭하면 해당 리스너에 기억된 콜백 함수를 실행된다.
- ⓐ forEach 메서드에 넘겨준 익명의 콜백함수ⓐ는 그 내부에서 외부 변수를 사용하지 않고 있으므로 클로저가 없고,
- ⓑ 의 addEventListener에 넘겨준 콜백 함수ⓑ에는 fruit이라는 외부 변수를 참조하고 있으므로 클로저가 있다.
콜백함수와 클로저(2) )
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[0]) // your choice is apple
- 공통함수로 쓰고자 콜백 함수를 외부로 꺼내어 alertFruit라는 변수에 담음
- 이렇게 하면 alertFruit를 직접 실행할 수 있다.
콜백 함수와 클로저(3) - bind 메서드로 값을 직접 넘겨줌 (클로저 사용 x) )
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));**
$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[0])
- bind를 활용해서 bind 메서드로 값을 직접 넘겨준 덕분에 클로저는 발생하지 않게된 반면 여러 가지 제약사항이 생김
콜백 함수와 클로저(4) - 콜백 함수를 고차함수를 사용 (클로저 o)
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
var **alertFruitbuilder** = function(fruit) {
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])
- ⓐ alerFruitBuilder 함수를 실행하면서 fruit 값을 인자로 전달함,
- 그러면 이 함수의 실행 결과가 다시 함수가 되며, 이렇게 반환된 함수를 리스너에 콜백 함수로써 전달한 것이다.
- 즉, alerFruitBuilder의 실행결과로 반환된 함수에는 클로저가 존재함
- 위 예시는 고차함수로 바꿔서 클로저를 적극적으로 활용한 방안이었다.
- 클로저는 외부 함수의 변수를 참조하는 내부 함수를 사용하여 접근 권한 제어(정보 은닉)를 할 수 있습니다.
- 정보 은닉(Information hiding) : 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 개념
- public : 클래스 내부, 외부에서 접근이 가능
- protected : 클래스 내부간 같은 상속 구조라면 접근이 가능
- private : 내부에서만 접근이 가능하며, 외부에 노출되지 않는 것
- 자바스크립트는 기본적으로 변수 자체에 이러한 접근 권한을 직접 부여하도록 설계돼 있지 않지만 그렇다고 접근 권한 제어가 불가능한 것은 아니다!!!
- 클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하는 것이 가능하다.
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
- outer함수를 종료할 때 inner 함수를 반환함으로써 outer함수의 지역변수인 a의 값을 외부에서도 읽을 수 있게 되었다.
- 이처럼 클로저를 활용하면 외부 스코프에서 함수 내부의 변수들 중 선택적으로 일부의 변수에 대한 접근 권한을 부여할 수 있다. 바로 return을 활용해서 말이다.
- 외부에 제공하고자 하는 정보들을 모아서 return 하고,
- 내부에서만 사용할 정보들은 return하지 않는 것으로 접근 권한 제어가 가능한 것이다.
- return한 변수들은 공개 멤버(public)가 되고, 그렇지 않은 변수들은 비공개 멤버(private)가 되는 것이다.
- 부분 적용 함수(partially applied function) : 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
-
addPartal 함수는 인자 5개를 미리 적용하고, 추후 추가적으로 인자들을 전달하면 모든 인자를 모아 원래의 함수가 실행되는 부분 적용 함수이다.
-
디바운스(debounce) : 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것
- 프론트엔드 성능 최적화에 큰 도움을 주는 기능 중 하나이다.
- 커링 함수(currying function) : 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것이다.
- 한번에 하나의 인자만 전달하는 것을 원칙으로함
- 또한, 중간 과정상의 함수를 실행한 결과는 그다음 인자를 받기 위해 대기만 하고, 마지막 인자가 전달되기 전까지는 원본 함수가 실행 되지 않는다.
- 반면에 부분 적용 함수는 여러 개의 인자를 전달할 수 있고, 실행 결과를 재실행 할때 원본 함수가 무조건 실행된다.
커링 함수(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)(10);
console.log(getMinWith10(8)); // 8
console.log(getMinWith10(25)); // 25
- 커링함수는 부분 적용함수와 달리 필요한 상황에 직접 만들어 쓰기 용이하다.
- 필요한 인자 개수만큼 함수를 만들어 계속 리턴 해주다가 마지막에 조합해서 리턴해주면 된다.
- 커링함수 단점 : 인자가 많아질 수록 가독성이 떨어진다.
가독성이 떨어지는 예시 - 커링 함수(2) )
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)(2)(3)(4)(5)); //5
위 코드를 화살표 함수를 사용해서 같은 내용을 한 줄로 줄이기 )
var curry5 = func => a => b => c => d => e => func(a,b,c,d,e);
console.log(getMax(1)(2)(3)(4)(5)); //5
- 화살표 함수를 구현하면 커링 함수를 이해하기 쉬워짐
- 화살표 순서에 따라 함수에 값을 차례로 넘겨주면 마지막에 func가 호출될 거라는 흐름이 파악됨
- 순서 : 각 단계에서 받은 인자들을 모두 마지막 단계에서 참조할 것이므로 가비지 컬렉터 들어가지 않고 메모리에 쌓였다가, 마지막 호출로 실행 컨텍스트가 종료된 후에야 비로서 한꺼번에 가비지 컬렉터의 수거 대상이 된다.
- 지연실행 : 다장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 하면 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 셈이 된다.
- 원하는 시점까지 지연시켰다가 실행하는 것이 요긴한 상황이라면 커링을 쓰기에 적합함
- 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
- 내부함수를 외부로 전달하는 방법에는 함수를 return하는 경우뿐 아니라 콜백으로 전달하는 경우도 포함됨
- 클로저는 그 본질이 메모리를 계속 차지하는 개념이므로, 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 않도록 관리해줄 필요가 있다.
정재남, 코어 자바스크립트