저번 시간에 이어 예제를 통해 실행 컨텍스트가 어떻게 생성되고 관리되는지 알아보자
var x = 1;
const y = 2;
function foo (a) {
var x = 3;
const y = 4;
function bar (b) {
const z = 5;
console.log(a + b + x + y + z);
}
bar(10);
}
foo(20);// 42
전역 객체는 전역 코드가 평가되기 이전에 생성된다. 전역 객체에는 빌트인 전역 프로퍼티와 전역 함수, 그리고 표준 빌트인 객체가 추가되며 동작 환경에 따라 클라이언트 사이드 Web API(DOM, BOM, Canvas XMLHttpRequest, fetch...) 또는 특정 환경을 위한 호스트 객체를 포함한다.
소스코드가 로드되면 js엔진은 전역 코드를 평가한다. 세부적인 생성 과정을 살펴보자.
먼저 전역 실행 컨텍스트를 생성하여 실행 컨텍스트 스택에 푸시한다.
전역 렉시컬 환경을 생성하고 전역 실행 컨텍스트에 바인딩한다.
전역 렉시컬 환경은 전역 스코프 역할을 하는 객체 환경 레코드(Object Environment Record)와 선언적 환경 레코드(Declarative Environment Record)로 구성되어 있다.
y에 바인딩 되어있는 uninitialized는 이런 의미이다. 실제로 uninitialized라는 값이 바인딩된 것이 아니다.
이후 [[GlobalThisValue]] 내부 슬롯에 this바인딩이 일어난다. 이후 외부 렉시컬 환경에 대한 참조가 결정된다.
현재 평가 중인 소스코드는 전역 코드로 외부 렉시컬 환경에 대한 참조는 null값이 할당된다.
이제 전역 코드가 순차적으로 실행되며 변수 할당문이 실행되어 변수에 값이 바인딩된다. 그리고 foo 함수가 호출된다.
foo 함수가 호출되면 전역 코드의 실행을 일시 중단하고 foo함수의 내부로 제어권이 이동한다. 그리고 함수의 코드가 평가된다.
이 과정을 거쳐 생성된 foo 함수 실행 컨텍스트와 렉시컬 환경은 다음과 같다.
생성 순서는 다음과 같다.
함수의 상위 스코프는 this와 달리 어디에 정의했는지에 따라 결정된다는 사실을 잘 기억하자.
foo함수는 전역에서 정의되었으므로 외부 렉시컬 환경 참조는 전역 렉시컬 환경을 가리킨다.
함수의 렉시컬 환경은 이런식으로 결정된다. 그리고 식별자를 검색할 경우 실행중인 실행 컨텍스트에서 식별자를 검색하며 만약 찾기 못할 경우 외부 렉시컬 환경에 대한 참조가 가리키는 렉시컬 환경으로 즉, 상위 스코프로 이동하며 식별자를 검색한다.
이것이 바로 스코프 체인의 원리이다.
함수의 상위 스코프를 결정하는 원리는 클로저의 원리와도 매우 밀접하므로 잘 기억해야 한다.
이제 런타임이 시작되어 foo 함수의 소스코드가 순차적으로 실행되기 시작한다.
var x = 1;
const y = 2;
function foo (a) {
var x = 3;
const y = 4;
function bar (b) {
const z = 5;
console.log(a + b + x + y + z);
}
bar(10);
}
foo(20);// 42
코드는 이제 bar 함수가 호출되어 bar 함수 내부로 코드의 제어권이 이동한다. 이후 foo 함수와 동일하게 bar 함수의 실행 컨텍스트와 렉시컬 환경이 생성된다.
외부 렉시컬 환경 참조와 this 바인딩을 잘 이해 한다면 위의 그림을 무리 없이 이해할 수 있다.
console.log(a + b + x + y + z);
코드가 실행되며 환경 기록과 외부 렉시컬 참조의 조합으로 스코프체인이 실행되며 변수의 값을 결정한다.
해당 코드가 종료되면 bar -> foo -> global 순으로 실행 컨텍스트가 실행 컨텍스트 스택에서 팝되며 코드가 종료되게 된다.