클로저란?
클로저는 함수가 속한 렉시컬 환경을 기억해서 함수가 렉스컬 스코프 밖에서 실행될 때에도 함수가 속한 스코프에 접근할 수 있게 하는 기능을 말한다.
function foo() {
var a = 2;
function bar(){
console.log(a); // 2
}
return bar;
}
var func = foo();
func();
GC(Garbage Collector)가 foo()의 참조를 없앨 것 같지만, bar()가 해당 스코프에 있는 a를 참조하기 때문에 없애지 않는다.
그러므로 스코프 외부에서 bar()가 실행되면 스코프를 기억하고 있기에 2가 출력된다.
이때, 클로저는 bar()에서 발생하며, 이는 스코프 외부에서 foo()의 스코프안에 있는 bar()를 참조하기 때문이다.
다시 말해서 클로저는 bar()가 되며 func에 담겨 밖에서 실행되고 렉시컬 스코프를 기억한다.
위의 예제 코드에서 bar()가 a의 값을 참조하는 부분이 있는데, 이 부분은 렉시컬 스코프 검색 규칙에 따르는 부분이며, 이는 클로저의 일부일 뿐이다.
함수가 선언된 렉시컬 스코프에 접근
또 다른 예시를 보자.
function foo () {
var a = 2;
function baz() {
console.log(a); // 2
}
bar(baz);
}
function bar(fn) {
fn(); // 이때 스코프가 발생!!
}
위의 코드에서는 baz를 bar에 넘기고, fn이라 명명된 함수를 호출한다.
foo 내부 스코프에 대한 fn은 클로저는 변수 a에 접근할 때 확인할 수 있다.
이는, 클로저는 호출된 함수가 원래 선언된 렉시컬 스코프에 계속해서 접근할 수 있도록 허용하는 특징을 보여준다.
위의 코드같이 함수 넘기기는 간접적인 방식으로 아래와 같이 작성할 수 있다.
var fn;
function foo() {
var a = 2;
function baz() {
console.log(a);
}
fn = baz; // 글로벌 변수인 fn에 baz를 할당
}
function bar(){
fn(); // 클로저 발생
}
foo();
bar(); // 2
어떤 방식으로 내부 함수를 자신이 속한 렉시컬 스코프 밖으로 수송하든 함수는 처음 선언된 곳의 스코프에 대한 참조를 유지한다.
반복문과 클로저
클로저를 설명하는 가장 흔하고 표준적인 사례인 for 반복문을 살펴보자.
function func() {
for (var i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i)
}, i*500);
}
}
func(); // 6 6 6 6 6
원래는 1부터 5까지 순서대로 나오는것이 의도였지만, 코드를 실행해보면 6이 5번 출력된다.
왜 그런것일까?
이유는 timeout 콜백 함수는 for문이 끝나고 나서 출력되기 때문이다.
setTimeout()을 반복문 안에서 돌리면 콜백함수가 계속해서 task queue에 쌓이게 되고 반복문이 끝나고 나서 call stack으로 돌아와 실행된다.
콜백함수는 클로저이기 때문에 상위 스코프에게 i의 값을 참조하고 상위 스코프인 func에서의 i는 이미 6까지 증가했기 때문에 6이 5번 출력되는 것이다.
그렇다면 해결법 있나?
당연히 있다.
1. 새로운 함수 스코프를 이용한 해결
for(var i = 1; i <= 5; i++){
(function (){
var j = i;
setTimeout( function timer(){
console.log(j);
}, j*1000);
})();
}
for(var i = 1; i <= 5; i++){
(function (j){
setTimeout( function timer(){
console.log(j);
}, j*1000);
})(i);
}
위에서 보다시피 두가지 방법에서 새로운 함수 스코프를 생성해 해결했다.
setTimeout()을 IIFE( Immediately Invoked Function Expression, 즉시실행함수 표현식 )으로 감싸게 되면 새로운 스코프를 형성하고 콜백함수가 j를 참조할 때 각 타이밍에 맞는 i값을 갖기에 원하는 결과를 얻을 수 있다.
2. 블록 스코프를 이용한 해결
for(var i = 1; i <= 5; i++){
(function (){
let j = i; // let으로 클로저를 위해 블록 스코프를 형성
setTimeout( function timer(){
console.log(j);
}, j*1000);
})(i);
}
for(let i = 1; i <= 5; i++){
(function (){
setTimeout( function timer(){
console.log(i);
}, i*1000);
})(i);
}
함수 스코프가 아닌 블록 스코프를 갖는 let을 이용해 for문 내의 새로운 스코프를 갖게하여 매 반복마다 새로운 i가 선언되고 끝나면 초기화가 된다.
이로인해 setTimeout()의 클로저인 콜백함수가 i를 참조하기 위해 상위 스코프를 검색할 때 블록 스코프에서 반복마다 선언 및 초기화된 i를 참조하는 것이다.
'JavaScript > Javascript' 카테고리의 다른 글
변수, var 그리고 블럭 스코프를 갖는 let, const (0) | 2023.10.02 |
---|---|
네이티브 객체와 호스트 객체 (0) | 2023.09.26 |
this 바인딩 (0) | 2023.09.22 |
호이스팅 (0) | 2023.09.21 |
스코프 (0) | 2023.09.21 |