Contents

모던 자바스크립트 Deep Dive 23장 - 실행 컨텍스트

소스코드의 타입

  1. 전역 코드 (global code): 전역에 존재하는 소스코드. 전역에 정의된 함수, 클래스의 내부 코드는 포함 X
    1. 전역 코드는 전역 변수를 관리하기 위해 최상위 전역 스코프 생성
    2. var 키워드로 선언된 전역 변수 + 함수 선언문으로 정의된 전역 함수를 전역 객체의 프로퍼티와 메서드로 바인딩 및 참조
    3. 전역 코드 평가 시 전역 실행 컨텍스트 생성
  2. 함수 코드 (function code): 함수 내부에 존재하는 소스코드. 함수 내부에 중첩된 함수, 클래스의 내부 코드는 포함 X
    1. 함수 코드는 지역 스코프를 생성
    2. 지역 변수, 매개변수, arguments 객체를 관리
    3. 전역 스코프에서부터 이어지는 스코프 체인으로 연결
    4. 함수 코드 평가 시 함수 실행 컨텍스트 생성
  3. eval 코드 (eval code): 빌트인 전역 함수인 eval 함수에 인수로 전달되어 실행되는 소스코드
    1. strict mode에서 독자적인 스코프 생성
    2. eval 코드 평가 시 eval 실행 컨텍스트 생성
  4. 모듈 코드 (module code): 모듈 내부에 존재하는 소스코드. 모듈 내부의 함수, 클래스의 내부 코드는 포함 X
    1. 모듈별로 독립적인 모듈 스코프 생성
    2. 모듈 코드 평가 시 모듈 실행 컨텍스트 생성

소스코드의 평가와 실행

소스코드 평가 과정은 실행 컨텍스트를 생성하고, 선언문만 먼저 실행해 생성된 변수나 함수 식별자를 키로 스코프에 등록한다.

런타임이 시작되면 소스코드 실행에 필요한 변수나 함수의 참조를 실행 컨텍스트가 관리하는 스코프에서 검색해서 얻는다. 변수 값의 변경 등 소스코드의 실행 결과는 다시 실행 컨텍스트가 관리하는 스코프에 등록된다.

실행 컨텍스트는 어떤 역할을 할까?

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); // 메서드 호출

0. 전역 객체 생성

전역 코드 평가 이전에 생성. 추가되는 것들은 아래와 같다.

  • 빌트인 전역 프로퍼티
  • 빌트인 전역 함수
  • 표준 빌트인 객체
  • (클라이언트 사이드라면) Web API를 위한 호스트 객체 (window 객체)

전역 객체 또한 예외없이 Object.prototype을 상속받기 때문에 프로토타입 체인의 일원

1. 전역 코드 평가

전역 코드를 실행하기 위한 준비.

  1. 소스코드 평가 과정에서는 선언문만 먼저 실행
  2. 전역 코드의 변수 선언문 + 함수 선언문이 먼저 실행되고,
  3. 그 결과로 생성된 전역 변수와 전역 함수가 실행 컨텍스트가 관리하는 전역 스코프에 등록

전역 코드 평가는 다음과 같은 순서로 실행된다.

  1. 전역 실행 컨텍스트 실행
  2. 전역 렉시컬 환경 생성
    1. 전역 환경 레코드 생성
      1. 객체 환경 레코드 생성
        1. var 키워드로 선언한 전역 변수
          1. 선언과 초기화가 동시에 진행
          2. 전역 코드 평가 시점에 식별자를 키로 등록한 다음 암묵적으로 undefined 바인딩
          3. 때문에 코드 실행 단계에서 변수 선언문 이전에도 참조 가능 (하지만 항상 undefined) -> 문제가 되는 변수 호이스팅이 발생하는 원인
          4. var 키워드로 선언한 변수에 할당한 함수 표현식도 동일하게 동작
        2. 함수 선언문으로 정의한 전역 함수
        3. 빌트인 전역 프로퍼티 (window.alert 등)
        4. 빌트인 전역 함수
        5. 표준 빌트인 객체
      2. 선언적 환경 레코드 생성
        1. let, const로 선언한 전역 변수
          1. 전역 객체의 프로퍼티가 되지 않기 떄문에 전역 객체의 프로퍼티로써 참조 불가
          2. const로 선언한 변수는 선언과 초기화가 분리되어 진행됨
          3. 초기화 단계 전(런타임에 해당 변수 선언문이 실행되기 전)까지 Temperal Dead Zone에 빠진다
        2. 마찬가지로 변수 호이스팅이 발생하지만 런타임에 변수 선언문이 실행되기 전까지 TDZ에 빠지기 때문에 참조할 수 없다는 큰 차이 존재
    2. this 바인딩
      1. this바인딩은 전역 환경 레코드와 함수 환경 레코드에만 존재
    3. 외부 렉시컬 환경에 대한 참조 결정
var bar = 2; // 이 코드가 없어도 밑의 console.log는 에러가 발생하지 않고 undefined 출력
{
   console.log(bar); // 2
   var bar = 3; 
}

let foo = 1;
{
   console.log(foo); // ReferenceError
   let foo = 2; // 지역변수가 호이스팅되지만 TDZ에 있기 때문에 참조 불가.
}

2. 전역 코드 실행

런타임의 시작, 전역 코드가 순차적으로 실행.

  1. 전역 변수에 값이 할당되고 함수가 호출
  2. 함수가 호출되면 순차적으로 실행되던 전역 코드는 잠깐 멈춰
    1. 코드 실행 순서가 변경되고 함수 내부로 진입

식별자 결정

식별자는 스코프가 다르면 같은 이름을 가질 수 있다 -> 어떤 스코프에 위치하는 식별자인지 판별 필요

실행 중인 컨텍스트에서 식별자를 검색하기 시작해서, 없다면 외부 렉시컬 환경 참조 (상위 스코프)에서 검색 재개

3. 함수 코드 평가

함수 호출이 되어 함수 내부로 진입하면 함수 코드 평가 과정을 거쳐 함수 코드 실행을 위한 준비.

  1. 매개변수와 지역 변수 선언문이 먼저 실행
  2. 그 결과로 생성된 매개변수와 지역 변수가 실행 컨텍스트가 관리하는 지역 스코프에 등록
  3. 함수 내부에서 지역 변수처럼 사용할 수 있는 arguments 객체가 생성되어 지역 스코프에 등록되고 this 바인딩도 결정

다음의 과정을 거쳐서 함수 실행 컨텍스트와 렉시컬 환경이 생성된다.

  1. 함수 실행 컨텍스트 생성 (스택의 가장 베이스)
  2. 함수 렉시컬 환경 생성 + 생성된 실행 컨텍스트에 바인딩
    1. 함수 환경 레코드 생성
    2. this 바인딩
    3. 외부 렉시컬 환경에 대한 참조 결정

4. 함수 코드 실행

런타임 다시 시작. 함수 코드가 순차적으로 실행.

  1. 매개변수와 지역 변수에 값 할당
  2. console.log() 메서드 호출
    1. console은 식별자 -> 스코프 체인 통한 검색 필요
    2. 하지만 console은 스코프 체인에 등록 X, 전역 객체의 프로퍼티로 존재
    3. 전역 객체의 프로퍼티는 마치 전역 변수처럼 전역 스코프를 통해 검색 가능해야 함을 의미
  3. log 프로퍼티를 console 객체의 프로토타입 체인을 통해 검색
    1. console.log 메서드에 인수로 전달된 표현식 a + x + y가 평가
    2. a, x, y 식별자는 스코프 체인을 통해 검색
    3. console.log 메서드 실행 종료되면 함수 코드 실행 종료
    4. 다시 함수 호출 이전으로 되돌아가 전역 코드 실행을 재개

코드가 실행되기 위해서는 스코프를 구분해서 식별자와 바인딩된 값이 관리되어야 한다.

또 중첩 관계에 의해 스코프 체인을 형성해서 식별자를 검색할 수 있어야 한다. (이 때 전역 객체의 프로퍼티도 전역 변수처럼 검색 가능해야 한다)

함수 호출이 종료되면 함수 호출 이전으로 되돌아가기 위해 현재 실행 중인 코드와 이전에 실행하던 코드를 구분해서 관리해야 한다.

코드가 실행되기 위해서는 스코프, 식별자, 코드 실행 순서의 관리가 필요하다.

  • 선언에 의해 생성된 모든 식별자를 스코프 구분해서 등록하고 상태 변화 관리
  • 중첩 관계에 의한 스코프 체인을 형성하고, 스코프 체인을 통해 상위 스코프로 이동해 식별자 검색이 가능해야 함
  • 현재 실행 중인 코드 실행 순서를 변경할 수 있어야 하고, 다시 되돌아갈 수도 있어야 함

실행 컨텍스트는 소스코드를 실행하는데 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리한다.

즉 실행 컨텍스트는 식별자 (변수, 함수, 클래스의 이름)을 등록하고 관리하는 스코프와 코드 실행 순서 관리를 구현한 내부 메커니즘으로, 모든 코드는 실행 컨텍스트를 통해 실행되고 관리된다.

  • 실행 컨텍스트의 렉시컬 환경 -> 식별자와 스코프를 관리
  • 코드 실행 순서 -> 실행 컨텍스트 스택으로 관리

실행 컨텍스트 스택

const x = 1;

function foo() {
   const y = 2;

   function bar() {
      const z = 3;
      console.log(x + y + z);
   }
   
   bar();
}

foo(); // 6

생성된 실행 컨텍스트는 스택 자료구조로 관리된다.

렉시컬 환경

const x = 1;

function foo() {
   const y = 2;
   console.log(x + y);
}

렉시컬 환경은 키와 값을 가지는 객체 형태의 스코프를 생성하여, 식별자를 키로 등록하고 식별자에 바인딩된 값을 관리한다.

  • 실행 컨텍스트는 LexicalEnvironment 컴포넌트와 VariableEnvironment 컴포넌트로 구성된다.
    • 초기에는 모두 하나의 동일한 렉시컬 환경을 참조
    • 이후 VariableEnvironment 컴포넌트를 위한 새로운 렉시컬 환경을 생성
      • 이 때부터는 서로 다른 렉시컬 환경을 가지는 경우도 존재
  • 렉시컬 환경은 EnvironmentRecord 컴포넌트와 OuterLexicalEnvironmentReference 컴포넌트로 구성된다.
    • EnvironmentRecord 컴포넌트는 스코프에 등록된 식별자를 등록하고, 등록된 식별자에 바인딩된 값을 관리하는 저장소
    • OuterLexicalEnvironmentReference 컴포넌트는 상위 스코프를 참조
      • 즉 해당 실행 컨텍스트를 생성한 소스코드를 포함하는 상위 코드의 렉시컬 환경
      • 외부 렉시컬 환경에 대한 참조를 통해 단방향 링크드 리스트인 스코프 체인 구성

실행 컨텍스트와 블록 레벨 스코프

  • var 키워드로 선언한 변수는 함수의 코드 블록만 지역 스코프로 인정한다.
  • let, const 키워드로 선언한 변수는 모든 코드 불록을 지역 스코프로 인정하는 블록 레벨 스코프를 따른다.
let x = 1;

if (true) {
   let x = 10;
   console.log(x); // 10
}

console.log(x); // 1