
목차
함수가 호출되는 위치가 아닌, 함수가 선언(정의)되는 위치에 의해 함수의 상위 스코프를 정적으로 결정하는 것이다.
const x = 1;
function outerfunc(){
const x = 10;
innerFunc();
}
function innerFunc(){
console.log(x);
}
outerFunc();
1outerFunc, innerFunc이 동일하게 전역이다. 둘의 정의 위치가 같은 것을 코드 상에서 확인할 수 있다.innerFunc은 outerFunc의 스코프에 존재하는 x(10)에 접근할 수 없고 전역에 존재하는 변수인 x(1)를 참조하게 된다. const x = 1;
function outerfunc() {
const x = 10;
function innerFunc() {
console.log(x);
}
innerFunc();
}
outerFunc();
10outerFunc의 정의 위치는 예제 코드 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 [강의] 스코프와 클로저
명쾌한 설명 감사합니다