자바스크립트는 함수 지향 언어입니다.
함수를 동적으로 생성할 수 있고,
함수를 인수로 넘길 수 있고,
생성된 곳이 아닌 곳에서 함수를 호출할 수 있습니다.
또한, 함수 내부에서 외부의 변수를 접근할 수 있습니다.
그런데, 생성 이후에 외부 변수가 변경된다면 어떨까요?
매개변수를 통해 함수를 넘기고 다른 곳에서 함수가 호출되면, 외부 변수를 사용할 때 어떻게 동작 할까요?
코드블록 {...}
안에서 선언한 변수는 코드블록 안에서만 사용할 수 있습니다
바{
let count = 0;
alert(count);
}
alert(count); // referenceError
if
, for
, while
등에서도 마찬가지로 코드블럭 안에서 선언한 변수는 안에서만 사용 가능합니다.
for
옆 괄호에서 선언한 변수도 블럭 안에 속하는 코드로 취급됩니다.
함수 내부에서 선언한 함수는 중첩(nested) 함수라고 부릅니다.
중첩 함수는 코드를 정돈하는데 사용할 수 있습니다.
function sayHiBye(firstName, lastName) {
function getFullName() {
return firstName + " " + lastName;
}
alert(`hi ${getFullName()}`);
alert(`bye ${getFullName()}`);
}
중첩 함수는 그 자체가 반환될 수 있습니다.
반환된 중첩 함수는 어디서든 호출해 사용할 수 있습니다.
이때에도 외부 변수에 접근할 수 있습니다.
function makeCounter() {
let count = 0;
return () => count++;
}
const counter = makeCounter();
console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2
여기에서 counter
를 여러 개 만들 때 과연 각 count
는 독립적인지 궁금해집니다.
JS의 실행중인 함수, 코드 블록, 스크립트 전체는 렉시컬 환경(Lexical Enviroment)이라 불리는 내부 숨김 연관 객체를 가집니다.
렉시컬 환경은 두 부분으로 구성됩니다.
this
값 등 기타 정보도 저장됩니다.내부적으로, 변수는 특수 내부 객체인 환경 레코드의 프로퍼티입니다.
'변수를 가져오거나 변경'하는 것은 환경 레코드의 프로퍼티를 가져오거나 변경함을 의미합니다.
왼쪽 코드로 인해 생성된 렉시컬 환경입니다.
전역 렉시컬 환경은 외부 참조가 없기 때문에 null
을 가르킵니다.
스크립트가 시작되면, 선언된 모든 변수가 렉시컬 환경에 올라갑니다.
phrase:<uninitalized>
에서 처럼 변수를 인지하지만 let
이전엔 참조할 수 없습니다.이후 변수가 선언되고, 할당되고, 변경됩니다.
렉시컬 환경은 명세에만 존재하는 이론상의 객체일 뿐 직접 조작할 수 없습니다.
함수 선언은 바로 초기화 됩니다.
따라서, 함수 선언문 이전에도 함수를 사용할 수 있습니다.
이와 같은 렉시컬 환경 상태를 확인할 수 있습니다.
함수 선언식 외에 함수 표현식은 해당되지 않습니다. ( 변수와 동일하게 초기화 됨 )
선언된 함수를 호출해 실행하면, 새로운 렉시컬 환경이 만들어집니다.
이 렉시컬 환경엔 넘겨 받은 매개변수와, 함수의 지역 변수가 저장됩니다.
say()
를 호출하면 다음과 같은 렉시컬 환경이 만들어집니다.
내부 렉시컬 환경은 외부 렉시컬 환경의 참조를 갖습니다. (빨간 화살표)
코드에서 변수에 접근하면, 내부 렉시컬 환경에서 검색합니다.
찾지 못하면 참조하는 외부 렉시컬로 확장합니다.
이 과정은 전역 렉시컬로 확장할 때 까지 반복합니다.
전역까지 찾지 못하면 엄격모드에선 에러가 발생합니다.
위 사진에서 phrase
변수는 내부 렉시컬에서 찾지 못해, 참조하는 외부 렉시컬로 확장되고,
전역 렉시컬에서 변수를 찾아 사용합니다.
아까 만든 makeCounter
로 돌아갑니다.
makeCounter
를 호출하면, 새로운 렉시컬이 생성되고, 변수들이 저장됩니다.
추가로, 함수 내부에서 중첩함수가 만들어집니다.
아직 실행되진 않고 생성만 되어 있습니다.
모든 함수는 [[Enviroment]]
라는 숨김 프로퍼티를 갖는데,
여기에, 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장됩니다.
숨김 프로퍼티는 직접 조작할 수 없습니다.
따라서, counter[[Enviroment]]
엔 makeCounter
렉시컬 환경에 대한 참조가 저장됩니다.
자신이 생성된 곳에 대한 환경을 기억할 수 있는 이유입니다.
중첩 함수 counter()
를 호출할 때 마다, 새로운 렉시컬 환경이 생성됩니다.
이 렉시컬 환경은 [[Enviroment]]
에 저장된 렉시컬을 외부 렉시컬로 참조합니다.
counter()
를 호출할 때 렉시컬 환경이 생성되고, 같은 외부 렉시컬을 참조합니다.
counter()
에서 사용하는 count
변수는 외부 렉시컬에 존재하고 해당 값을 변경합니다.
따라서, counter()
를 호출 할 때마다 count
의 값이 2
,3
... 으로 증가합니다.
또한, makeCount
를 여러번 호출하면, 호출마다 다른 렉시컬이 생기고,
중첩 함수 counter
는 해당 렉시컬을 참조합니다.
때문에, 다음과 같은 결과를 만듭니다.
function makeCounter() {
let count = 0;
return () => count++;
}
const counter = makeCounter();
console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2
const newCounter = makeCounter();
console.log(newCounter()); // 0
클로저 : 이 처럼 외부 변수에 접근할 수 있는 함수를 말합니다.
호출이 끝나면 함수에 대응하는 렉시컬은 메모리에서 제거됩니다.
즉, 함수와 관련된 변수는 모두 제거됩니다.
함수 호출이 끝나면, 관련 변수를 참조할 수 없는 이유가 이 때문입니다.
자바스크립트의 모든 객체는 (렉시컬 환경 포함) 도달 가능한 상태일 때, 메모리에 유지됩니다.
하지만, 함수의 호출이 끝나도 중첩 함수는 렉시컬에 도달할 수 있습니다.
중첩 함수의 [[Enviroment]]
프로퍼티의 외부 함수 렉시컬 환경이 참조되기 때문입니다.
때문에, 함수의 호출이 끝나도 렉시컬 환경이 메모리에 유지됩니다.
중첩 함수가 사라지고 나서야 렉시컬 환경도 메모리에서 제거됩니다.
이처럼 이론상으론 외부 변수 모두 메모리에 유지됩니다.
하지만, 자바스크립트 엔진이 이를 최적화하여 변수 사용을 분석하고 사용하지 않는다면 메모리에서 제거합니다.
debugger
를 통해 사용하지않는 외부변수가 디버거에서 확인할 수 없음을 알 수 있습니다.
이러한, 변수 최적화는 디버깅 이슈를 발생시키곤 합니다.
원인을 찾는 데 오랜시간을 보내지 않으려면, 이러한 V8 만의 부작용을 미리 아는 것이 좋습니다.