(<자바스크립트 셀프 QnA> 시리즈에 작성된 포스팅들은 각 주제에 해당하는 <모던 자바스크립트 딥다이브> 챕터를 읽으며 요약한 내용입니다. 더 자세한 내용은 <모던 자바스크립트 딥다이브>를 참고해주세요.)
모던 자바스크립트 딥다이브
Q1 렉시컬 스코프란 무엇인가요?
- 자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프(정적 스코프)라 한다.
- 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다.
- 함수는 자신의 내부 슬롯
[[Environment]]
에 자신의 정의된 환경, 즉 상위 스코프의 참조를 저장한다.
- 함수 객체의 내부 슬롯
[[Environment]]
에는 함수 정의가 평가된 시점, 즉 전역 코드 평가 시점에 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 저장된다.
- 함수 객체의 내부 슬롯
[[Environment]]
에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프이다. 자신이 호출되었을 때 생성될 함수 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장될 참조값이다. 함수 객체는 내부 슬롯 [[Environment]]
에 저장한 렉시컬 환경의 참조, 즉 상위 스코프를 자신이 존재하는 한 기억한다. (자바스크립트의 모든 함수는 자신의 상위 스코프를 기억한다.)
Q2 함수 코드 평가 순서에 대해 설명하세요.
- 함수 실행 컨텍스트 생성 > 함수 렉시컬 환경 생성 > 함수 환경 레코드 생성 > this 바인딩 > 외부 렉시컬 환경에 대한 참조 결정
Q3 클로저에 대해 설명하세요.
- 콜 스택에서 이미 팝되어 제거된 실행 콘텍스트의 지역변수가 리턴된 내부 함수에 의해 부활이라도 한 듯 동장되는 현상.
- 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있고, 이러한 중첩 함수를 클로저라고 부른다.
const x = 1;
function outer() {
const x = 10;
const inner = function() { console.log(x) }
return inner;
}
const innerFunc = outer();
innerFunc();
- outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다. outer 함수의 렉시컬 환경은 inner 함수의
[[Environment]]
내부 슬롯에 의해 참조되고 있고 inner 함수는 전역 변수 innerFunc에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 되지 않기 때문이다. 가비지 컬렉터는 누군가가 참조하고 있는 메모리 공간을 함부로 해제하지 않는다.
- 이론적으로 모든 함수는 클로저다.
- 하지만,상위 스코프의 어떤 식별자도 참조하지 않는 경우 대부분의 모던 브라우저는 최적화를 통해 상위 스코프를 기억하지 않는다. 참조하지 않는 식별자를 기억하는 것은 메모리 낭비이기 때문!
- 더 나아가, 상위 스코프의 식별자를 참조하지만 외부 함수보다 일찍 소멸돼 생명주기가 종료되는 내부함수 역시 클로저의 본질에 부합하지 않는다.
- 결국 클로저는 중첩함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다.
- 클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수라고 부른다. 클로저란 "함수가 자유변수에 대해 닫혀있다"라는 의미이다. 클로저는 결국 "자유변수에 묶여있는 함수"라고 할 수도 있겠다.
Q4 클로저는 어디에 활용되나요?
- 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다. 클로저를 사용하면 상태를 안전하게 은닉하고, 특정 함수에게만 상태 변경을 허용할 수 있다.
function counter() {
let num = 0;
return {
increase() {
return ++num;
},
decrease() {
return num > 0 ? --num : 0;
}
}
}
- num은 외부에서 접근이 불가하다.
- 외부 상태 변경이나 가변 데이터를 피하고 불변성을 지향하는 함수형 프로그래밍에서 부수 효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.
- 클로저 함수를 포함하고 있는 외부함수를 호출해 클로저를 반환할 떄 반환된 함수는 자신만의 독립된 렉시컬 환경을 갖는다.
const counter1 = counter();
const counter2 = counter();
- 즉, 전역 변수 counter1과 counter2에 할당된 함수는 각각 자신만의 독립된 렉시컬 환경을 갖기 때문에 카운트를 유지하기 위한 자유변수 num을 공유하지 않아 카운터의 증감이 연동되지 않는다.
- 클로저는 캡슐화와 정보은닉을 위해서도 활용된다.
- 캡슐화란 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다.
그 외 활용사례
const funcs = [];
for(let i = 0; i < 3; i++){
funcs[i] = function() { return i };
}
for(let i = 0; i < funcs.length; i++){
console.log(funcs[i]());
}
- 이 때 var를 사용해주면 안 된다. var와 같은 경우 함수 스코프를 따르기 때문에 for문의 코드블록이 무시 된다.