소스코드의 타입
전역 코드
- 전역에 존재하는 소스코드
- 전역 스코프 생성 => 전역 변수 관리
- 전역 코드가 평가되면 전역 실행 컨텍스트 생성됨
함수 코드
- 함수 내부에 존재하는 소스코드
- 지역 스코프 생성 => 지역 변수, 매개변수, arguments 객체 관리
- 함수 코드가 평가되면 함수 실행 컨텍스트 생성됨
eval 코드
- eval 함수에 인수로 전달되어 실행되는 소스코드
- strict mode에서 자신만의 독자적인 스코프 생성
- eval 코드가 평가되면 eval 실행 컨텍스트 생성됨
모듈 코드
- 모듈 내부에 존재하는 소스코드
- 모듈별로 독자적인 모듈 스코프 생성
- 모듈 코드가 평가되면 모듈 실행 컨텍스트 생성됨
소스코드의 평가와 실행
-
소스코드의 평가
- 실행 컨텍스트 생성
- 변수/함수 선언문 실행
- 생성된 변수/함수 식별자를 실행 컨텍스트가 관리하는 스코프에 등록
-
소스코드의 실행
- 선언문을 제외한 소스코드 순차적 실행 (런타임 시작)
- 변수/함수의 참조를 스코프에서 검색하여 취득
- 소스코드 실행 결과는 다시 스코프에 등록됨
var x;
x = 1;
위 예제의 처리 과정은 다음과 같다.
- 소스코드 평가 과정에서 변수 선언문
var x;
먼저 실행
- 생성된 변수 식별자 x => 실행 컨텍스트가 관리하는 스코프에 등록된 후 undefined로 초기화
- 변수 할당문
x = 1;
실행 => 스코프에 x 변수가 등록되어 있는지 확인 후 할당
- 할당 결과를 실행 컨텍스트에 등록하여 관리
실행 컨텍스트의 역할
아래 예제를 자바스크립트 엔진이 어떻게 평가하고 실행하는지 살펴보자.
const x = 1;
const y = 2;
function foo(a) {
const x = 10;
const y = 20;
console.log(a + x + y);
}
foo(100);
console.log(x + y);
-
전역 코드 평가
- 전역 변수/함수 선언문 먼저 실행
- 생성된 전역 변수 x, y와 전역 함수 foo를 전역 스코프에 등록
-
전역 코드 실행
- 전역 변수 x, y에 값이 할당되고 전역 함수 foo가 호출됨
- 전역 함수 foo가 호출되면 전역 코드의 실행을 일시 중단하고 함수 내부로 진입
-
함수 코드 평가
- 매개변수, 지역 변수 선언문 먼저 실행
- 생성된 매개변수 a, 지역 변수 x, y를 지역 스코프에 등록
- arguments 객체가 생성되어 지역 스코프에 등록
- this 바인딩 결정
-
함수 코드 실행
- 매개변수 a와 지역 변수 x, y에 값이 할당됨
- console.log 메소드 호출됨
- 인수로 전달된
a+x+y
평가 => 스코프 체인을 통해 식별자 검색하여 참조
- 함수 종료 후 다시 전역 코드 실행
코드가 실행되려면 스코프, 식별자, 코드 실행 순서 등의 관리가 필요하고, 이 모든 것을 관리하는 게 바로 실행 컨텍스트다.
실행 컨텍스트는 식별자를 등록/관리하는 스코프와 코드 실행 순서 관리를 구현한 내부 메커니즘이다.
모든 코드는 실행 컨텍스트를 통해 실행되고 관리된다.
- 식별자와 스코프 => 렉시컬 환경으로 관리
- 코드 실행 순서 => 실행 컨텍스트 스택으로 관리
실행 컨텍스트 스택
- 소스코드 평가 => 실행 컨텍스트 생성
- 실행 컨텍스트는 스택 자료구조로 관리됨
- 코드의 실행 순서 관리
- 실행 컨텍스트 스택의 최상위 => 현재 실행 중인 코드의 실행 컨텍스트가 위치
const x = 1;
function foo() {
const y = 2;
function bar() {
const z = 3;
console.log(x + y + z);
}
bar();
}
foo();
-
전역 코드의 평가와 실행
- 전역 코드 평가 => 전역 실행 컨텍스트 생성 후 실행 컨텍스트 스택에 push
- 전역 변수 x, 전역 변수 foo => 실행 컨텍스트에 등록
- 전역 코드 실행 => 전역 변수 x에 값이 할당되고 전역 함수 foo 호출
-
foo 함수 코드의 평가와 실행
- foo 함수 코드 평가 => foo 함수 실행 컨텍스트 생성 후 실행 컨텍스트 스택에 push
- 지역 변수 y, 중첩 함수 bar => 실행 컨텍스트에 등록
- 함수 코드 실행 => 지역 변수 y에 값이 할당되고 중첩 함수 bar 호출
-
bar 함수 코드의 평가와 실행
- bar 함수 코드 평가 => bar 함수 실행 컨텍스트 생성 후 실행 컨텍스트 스택에 push
- 지역 변수 z => 실행 컨텍스트에 등록
- 함수 코드 실행 => 지역 변수 z에 값이 할당되고 console.log 메소드 호출
- bar 함수 종료
-
foo 함수 코드로 복귀
- bar 함수 실행 컨텍스트 => 실행 컨텍스트 스택에서 pop하여 제거
- foo 함수 종료
-
전역 코드로 복귀
- foo 함수 실행 컨텍스트 => 실행 컨텍스트 스택에서 pop하여 제거
- 전역 실행 컨텍스트 => 실행 컨텍스트 스택에서 pop하여 제거
렉시컬 환경
- 실행 컨텍스트를 구성하는 컴포넌트
- 식별자, 식별자에 바인딩된 값, 상위 스코프에 대한 참조를 기록하는 자료구조 (식별자, 스코프 관리)
- EnvironmentRecord, OuterLexicalEnvironmentReference 로 구성
EnvironmentRecord (환경 레코드)
- 스코프에 포함된 식별자 등록
- 식별자에 바인딩된 값 관리
OuterLexicalEnvironmentReference (외부 렉시컬 환경에 대한 참조)
- 상위 스코프를 가리킴
- 스코프 체인 (단방향 링크드 리스트) 구현
실행 컨텍스트의 생성과 식별자 검색 과정
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);
1. 전역 객체 생성
- 빌트인 전역 프로퍼티/함수, 표준 빌트인 객체, 클라이언트 사이드 Web API, 호스트 객체 추가됨
- Object.prototype 을 상속받음 (프로토타입 체인의 일원)
2. 전역 코드 평가
-
전역 실행 컨텍스트 생성 => 실행 컨텍스트 스택에 push
-
전역 렉시컬 환경 생성 => 전역 실행 컨텍스트에 바인딩
2-1. 전역 환경 레코드 생성
-
객체 환경 레코드 생성 => BindingObject 객체 (전역 객체) 와 연결
- var로 선언한 변수, 함수 선언문으로 정의한 함수 => 전역 객체의 프로퍼티/메소드가 됨
-
선언적 환경 레코드 생성
- let, const로 선언한 변수, 함수 표현식으로 정의한 함수 등록/관리
2-2. this 바인딩
- 전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 this 바인딩 (전역 객체)
- 전역 환경 레코드, 함수 환경 레코드에만 존재 (객체 환경 레코드, 선언적 환경 레코드에는 X)
2-3. 외부 렉시컬 환경에 대한 참조 결정
- 상위 스코프를 가리킴
- 전역 코드라면 null (전역 렉시컬 환경 = 스코프 체인의 종점에 존재)
3. 전역 코드 실행
- 전역 실행 컨텍스트의 전역 렉시컬 환경에서 식별자 검색 => 식별자 결정
- 전역 변수 x, y에 값 할당
- foo 함수 호출
4. foo 함수 코드 평가
-
함수 실행 컨텍스트 생성
-
함수 렉시컬 환경 생성 => foo 함수 실행 컨텍스트에 바인딩
2-1. 함수 환경 레코드 생성
- 매개변수, arguments 객체, 함수 내부의 지역 변수나 중첩 함수 등록/관리
2-2. this 바인딩
- 함수 환경 레코드의 [[ThisValue]] 내부 슬롯에 this 바인딩 (함수 호출 방식에 따라 결정)
2-3. 외부 렉시컬 환경에 대한 참조 결정
- 함수의 상위 스코프 => 함수 객체의 내부 슬롯 [[Environment]]에 저장됨
5. foo 함수 코드 실행
- foo 함수 렉시컬 환경에서 식별자 검색 => 없으면 스코프 체인 타고 상위 스코프에서 검색
- 매개변수에 인수 할당
- 지역 변수 x, y에 값 할당
- bar 함수 호출
6. bar 함수 코드 평가
7. bar 함수 코드 실행
- 매개변수에 인수 할당
- 지역 변수 z에 값 할당
console.log(a + b + x + y + z);
실행
- console 식별자 검색
(전역 렉시컬 환경 - 객체 환경 레코드의 BindingObject를 통해 전역 객체에서 찾을 수 있음)
- log 메소드 검색 (console 객체가 직접 소유하는 프로퍼티)
- a, b, x, y, z 식별자 검색 => 표현식 평가
- 평가 후 생성한값을 console.log 메소드에 전달하여 호출
8. bar 함수 코드 실행 종료
- 실행 컨텍스트 스택에서 bar 함수 실행 컨텍스트 제거
- 이때 bar 함수 렉시컬 환경은 소멸하지 않음 (실행 컨텍스트에 의해 참조되지만 독립적인 객체)
9. foo 함수 코드 실행 종료
- 실행 컨텍스트 스택에서 foo 함수 실행 컨텍스트 제거
10. 전역 코드 실행 종료
- 실행 컨텍스트 스택에서 전역 실행 컨텍스트 제거
실행 컨텍스트와 블록 레벨 스코프
let x = 1;
if (true) {
let x = 10;
console.log(x);
}
console.log(x);
- 코드 블록 실행
- 선언적 환경 레코드를 갖는 렉시컬 환경 새롭게 생성 => 기존 렉시컬 환경 교체
- 새로 생성된 블록 렉시컬 환경의 외부 렉시컬 환경에 대한 참조 => 이전의 렉시컬 환경을 가리킴
- 코드 블록 종료 => 코드 블록 실행 이전의 렉시컬 환경으로 복귀
블록 레벨 스코프를 생성하는 모든 블록문에 적용이 되며, for문 같은 경우는 코드 블록이 반복해서 실행될 때마다 매번 새로운 렉시컬 환경이 생성된다.