목차
함수가 호출되는 위치가 아닌, 함수가 선언(정의)되는 위치에 의해 함수의 상위 스코프를 정적으로 결정하는 것이다.
const x = 1;
function outerfunc(){
const x = 10;
innerFunc();
}
function innerFunc(){
console.log(x);
}
outerFunc();
1
outerFunc
, innerFunc
이 동일하게 전역이다. 둘의 정의 위치가 같은 것을 코드 상에서 확인할 수 있다.innerFunc
은 outerFunc
의 스코프에 존재하는 x
(10)에 접근할 수 없고 전역에 존재하는 변수인 x
(1)를 참조하게 된다. const x = 1;
function outerfunc() {
const x = 10;
function innerFunc() {
console.log(x);
}
innerFunc();
}
outerFunc();
10
outerFunc
의 정의 위치는 예제 코드 1과 다름이 없으므로 outerFunc
의 렉시컬 스코프는 전역이다. innerFunc
의 정의 위치는 outerFunc
의 내부이므로, innerFunc
의 렉시컬 스코프는 outerFunc
(의 렉시컬 환경)이다.innerFunc
은 자신의 상위 스코프인 outerFunc
의 x
(10)을 참조하게 된다.innerFunc
)가 외부 함수(outerFunc
)의 식별자를 참조하고는 있으나, 외부 함수보다 중첩 함수가 더 생명 주기가 짧기 때문이다.const x = 1;
function outer() {
const x = 10;
const inner = function() { console.log(x); }
return inner;
}
const innerFunc = outer();
innerFunc();
outer
함수는 outer
를 호출하면 중첩함수 inner
을 반환하고 생명주기를 마감한다.inner
함수가 outer
함수(의 렉시컬 환경)를 상위 스코프로 저장하고 있으므로,outer
함수의 렉시컬 환경에 대한 참조가 끊어지지 않고,outer
함수의 렉시컬 환경이 Garbage Collection의 대상이 되지 않으므로outer
함수의 렉시컬 환경이 유지된다.inner
함수는 outer
함수 스코프의 지역 변수 x
를 참조할 수 있게 된다.innerFunc
의 실행결과는 10
이 된다.조금 더 복잡한 예제를 살펴 보자. 클로저 관련 스레드에 멘토님이 올려주신 예제들이다.😎
function counting1() {
let i = 0;
for(i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 100);
}
}
counting1();
setTimeout
이 참조하고 있는 i
는 counting1
스코프의 i
이다.setTimeout
의 콜백함수가 실행될 때 참조하는 i
의 값은 이미 5
가 되어있다.5
가 다섯 번 찍히게 된다.function counting2() {
let i = 0;
for(let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 100);
}
}
counting2();
setTimeout
이 참조하고 있는 i
는 counting2
스코프의 i
가 아닌, for loop
스코프의 i
이다.setTimeout
의 콜백함수가 실행될 때 각각의 for loop
블록 스코프를 참조하도록 클로저가 생긴다.(i=0
부터 i=4
까지 각각의 setTimeout
의 콜백함수가 참조하고 있다.)0 1 2 3 4
가 순차적으로 찍힌다.함수 호출 횟수를 카운팅하는 함수를 다음과 같이 구현했다고 생각해보자.
let num = 0;
const increase = function() {
return ++num;
}
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
주석으로 달아놓은 결과와 같이, 코드는 잘 동작한다. 하지만 오류를 발생시킬 가능성이 큰 코드이다. 카운팅을 기록하는 변수 num
이 전역 변수로 관리 되어 누구나 접근 가능하고 변경 가능하기 때문이다. increase
함수를 호출 할 때만 num
을 증가시킬 수 있어야 바람직하다.
클로저를 이용하면 다음과 같이 구현할 수 있다.
const increase = (function () {
let num = 0;
return function () {
return ++num;
}
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
이렇게 되면 IIFE의 변수 num
은 오직 increase
만 접근할 수 있게 된다. IIFE는 오직 한 번 실행되므로 num
이 increase
호출 시마다 0으로 초기화 한다고 오해해서는 안된다.
increase
의 렉시컬 스코프는 자신이 정의된 위치인 IIFE이며, IIFE(렉시컬 환경)의 num
을 참조하고 있다.
생각보다 클로저를 이해하기 위해 학습해야 할 것들이 많았지만, 얕은 수준에서라도 이해하기 위해 클로저를 주제로 TIL을 작성해 보았다.
추후에 실행 컨텍스트, 비동기 처리를 어떻게 하는지를 학습하게 된다면 좀더 깊은 수준의 이해를 할 수 있을 것 같다.
콜 스택, Web API, Task Queue와 함께 비동기 처리 이해라던가, 실행 컨텍스트 같은 배경 지식을 좀 더 흡수하고 나면 더 명확한 표현으로 글을 쓸 수 있을 것 같다.
마침 이번 주 스터디 주제를 실행 컨텍스트로 잡았으니, 학습 후에 좀 더 분명한 용어로 이 글을 수정해보려고 한다!
아하 모먼트가 곧 찾아올 것 같다😀
모던 자바스크립트 딥다이브 24장 클로저
프로그래머스 프론트엔드 데브코스 Day1 [강의] 스코프와 클로저
명쾌한 설명 감사합니다