정재남,『코어자바스크립트』를 읽고 정리한 내용입니다. 이해가 부족한 부분은 책과 동일하게 작성하였습니다.
"클로저는 함수와 그 함수가 선언될 당시의 Lexical Environment의 상호관계에 따른 현상"
어떤 함수 A에서 선언한 변수 a를 참조하는 내부 변수 B를 외부로 전달할 경우,
A의 실행컨텍스트가 종료된 이후에도 변수a가 사라지지 않는 현상
이미지출처 - 버섯님의 블로그💛
1) let 선언 변수는 호이스팅 되므로 렉시컬 환경에는 올라가지만 초기화는 되지 않는다.
2) 함수선언은 바로 초기화 된다.
3)let amy;
에서 할당이 안되어있기 때문에 undefined이다.
4)amy=29;
로 할당하고
5) plusAge(1)에서 새로운 렉시컬 환경이 생성된다. 현재 내부 렉시컬 환경은 외부 렉시컬 환경을 참조한다.
(내부 렉시컬 환경에서 amy를 찾을 수 없어 전역 렉시컬 환경에서 찾는다)
즉, 함수가 생성될 당시의 외부 변수(amy)를 기억하고, 생성 이후에도 계속 접근이 가능하다.
클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다. 그런데 다른 언어에서는 일반함수와 클로저함수를 구분해서 사용한다. 근데 JS는 다 클로저 함수라서 개념 구분하려면 어렵게 느껴진다.
function add1(a,b) {
return a + b;
}
👽
let poison = 0; // 🔥add2의 외부변수
function add2(a,b) {
return a + b + poison;
}
add1(6, 11); //17
add2(6, 11); //17
poison = 3;
add1(6, 11); //17
add2(6, 11); //20
🔥 함수가 선언된 것 만 보고서는 함수 내부에 poison
이 없는지 있는지 모르기 때문에
중간에 누가 poison을 3으로 변경한 경우, 갑자기 17이 아니라 20이라는 예상치 못한 값을 얻게 된다.
모든 자바스크립트 함수는 클로저 함수가 될 수 있다. 이미 모두 클로저라고 말 할 수 도있다. 그러니까 더 어려운거다. 클로저인거랑 아닌거를 구분할일이 없으니까.
var outer = function () {
var a = 1;
var inner = function () {
console.log(++a);//2
};
inner();
};
outer();
// 2출력
inner
함수 내부에서는 변수 a를 선언하지 않았기 때문에 상위 컨텍스트인 outer
의 LexicalEnvironment에 접근해 변수a를 참조한다. outer
함수의 실행컨텍스트가 종료되면, LexicalEnvironment에 저장된 식별자(a, inner)에 대한 참조를 지운다.var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner ();
};
var outer2 = outer();
console.log(outer2);
// 2출력
return inner();
inner함수를 실행한 결과를 리턴한다.outer
함수의 실행컨텍스트가 종료된 시점에는 a변수를 참조하는 대상이 없어진다.🤔"예제 1.1 과 1.2는 일반적인 함수 및 내부함수에서의 동작과 차이가 없다. 클로저가 아니다.
그리고 공통적으로 outer함수의 실행 컨텍스트가 종료되기 이전에 inner 함수의 실행 컨텍스트가 종료돼 있으며, 이후 별도로 inner함수를 호출 할 수 없다.
그럼 outer의 실행 컨텍스트가 종료된 후에도 inner 함수를 호출할 수 있게 만들면 어떨까?
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner; ---->//inner함수 자체를 return!!
};
var outer2 = outer();
console.log(outer2());
// 2 출력
console.log(outer2());
// 3 출력
return inner
를 통해 함수 자체를 반환했다. outer의 실행 컨텍스트가 종료될 때,
outer2의 변수는 outer의 실행결과인 inner함수를 참조하게 된다.
즉, outer2를 호출하면 inner함수가 실행된다.
inner함수는 outer함수 내부에서 선언되었으므로, outer함수의 LexicalEnvironment가 담긴다.
스코프 체이닝에 따라 outer에서 선언한 변수a에 접근하여 1만큼 증가 시킨 2를 반환하며 inner의 실행컨텍스트가 종료된다.
outer2를 호출하면 같은 방식으로 a의 값이 1 증가되어 3이 출력된다.
가비지컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않기 때문에 outer함수가 종료되어도 outer함수는 inner함수의 변수 참조 때문에 GC의 대상이 되지 않는다.
클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우, A의 실행컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상
🤪주의사항!
외부로 전달
이 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.creatElement('button');
button.innerText = 'click';
button.addEventListener('click', function(){
console.log(++count, 'times clicked');
});
document.body.appendChild(button);
})();
메모리 소모는 클로저의 본질적인 특성 중 하나이다. 이러한 특성을 정확히 이해하고 잘 활용하도록 하자.
'메모리 누수'라는 표현은 개발자의 의도와는 달리 어떤값의 참조 카운트가 0이 되지 않아 GC의 수거대상이 않는 경우에는 맞는 표현이다.
하지만, 이런 메모리 누수는 최근 JS엔진에서는 거의 발생하지 않는다.
→ 지역변수의 필요성이 사라졌다면 참조 카운트를 0으로 만들어주면 된다.
그렇다면, 참조 카운트를 0으로 만들어주는 방법은?
식별자에 참조형이 아닌 기본형 데이터(보통 null이나 undefiend)를 할당하면 된다.
이어서 실제로 클로저를 어떻게 사용하는지 활용 사례에 대해 살펴보겠습니다 : )
[참고한자료]
정재남, 『코어자바스크립트』, 위키북스(2019)
클로저 - JavaScript | MDN
https://amyhyemi.tistory.com/142
https://youtu.be/KmpofpqkitA