ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JavaScript 의 execution context (실행 컨텍스트) 리뷰하기
    JavaScript 2023. 3. 4. 19:43

    C, C++ 만 알고 있던 나에게 가장 큰 애를 먹인 클로저를 리뷰하기에 앞서서,

    자바스크립트의 핵심 개념이지만 아 그런갑다 하고 넘긴 실행 컨텍스트에 대해 깊게 파보려고 한다.

     

    실행 컨텍스트를 잘 이해해야 클로저의 동작 방식을 이해할 수 있다고 하니까.

     


    출처 : 모던 자바스크립트 Deep Dive: 자바스크립트의 기본 개념과 동작 원리 / 이웅모 지음

     

    소스코드의 "평가" 와 "실행"

     

    기본적으로 소스코드는 모두 평가 -> 실행 의 단계를 거친다.

    그리고 그 과정을 실행 컨텍스트가 관리한다.

     

    소스코드의 평가는 간단히 말해 준비 단계이고, 평가가 끝나면 비로소 런타임이 시작되어 소스코드가 순차적으로 실행되는 구조.

     

    코드 실행 순서를 관리하는 실행 컨텍스트 스택의 흐름은 다음과 같다.

     

    const x = 1;
    
    function foo() {
    	const y = 2;
        
        function bar() {
        	const z = 3;
            console.log(x + y + z);
        }
        bar();
    }
    
    foo(); // 6
    3. bar 함수 실행 컨텍스트
    2. foo 함수 실행 컨텍스트
    1. 전역 실행 컨텍스트

    table 1. 실행 컨텍스트 스택 (혹은 콜 스택)

     

    ex) 전역 실행 컨텍스트의 스코프

    전역 실행 컨텍스트 (평가)
    x undefined
    foo() 등록

    table 2. 전역 코드 평가. 전역 변수와 함수 선언문이 먼저 실행된다.

    전역 실행 컨텍스트 (실행)
    x 1
    foo() 호출 (push 2)

    table 3. 전역 코드 실행. 전역 코드가 순차적으로 실행되며, 변수에 값이 할당되고 함수가 호출된다. 이 때 foo() 함수가 호출되는 시점에 실행 컨텍스트 스택에 2번이 push 되며 foo() 함수 코드 평가가 시작된다 !

     

    이런 흐름으로 스택에 각각의 실행 컨텍스트들이 push 되고, 끝나면 pop 된다.

     

    위와 같이, 실행 컨텍스트 스택은 코드의 실행 순서를 관리하는 것. 전역부터 시작하여 위와 같은 순서로 실행 컨텍스트 스택에 쌓여, 항상 최상위 실행 컨텍스트(running execution context) 를 실행한다.

     

     


     

    렉시컬 환경

     

     렉시컬 환경은 실행 컨텍스트 스택에 쌓인 각각의 실행 컨텍스트를 구성하는  컴포넌트로, 위에서는 스코프만으로 간단하게 표현했지만 사실은 좀더 복잡하다. (많이)

     

    복잡한 자료구조이지만, 이걸 이해해야 클로저를 제대로 이해할 수 있다.

     

    우선 기본적인 구조는 다음과 같다.

     

    Lexical Environment
    Environment Record ...
    Outer Lexical Environment Reference next (상위 스코프)

    table 4. 환경 레코드 : 스코프에 포함된 식별자를 등록, 바인딩된 값을 관리하는 저장소. 외부 참조 : 상위 스코프를 가리킴. 

     

    위의 구조를 통해 스코프 체인이 단방향 Linked list 로 구현되었음을 알 수 있다 ! 생각보다 할만하네

     

    실제로 각각의 실행 컨텍스트가 생성되고 코드 실행 결과가 관리되는 순서는 다음과 같다.

     

    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

    1. 전역 객체 생성 (Web API 포함)

    2. 전역 코드 평가

      2.1. 전역 실행 컨텍스트 생성

      2.2. 전역 렉시컬 환경 생성

        2.2.1. 전역 환경 레코드 생성

           2.2.1.1. 객체 환경 레코드 생성

           2.2.1.2. 선언적 환경 레코드 생성

        2.2.2. this 바인딩

        2.2.3. 외부 렉시컬 환경에 대한 참조 결정

    3. 전역 코드 실행 (실행 중 foo() 를 호출하면 전역 코드 실행이 중단되고 foo() 코드 평가부터 시작 !)

    ...

     

    대략적으로는 위와 같은 순서로 진행된다.

    2.1 = 실행 컨텍스트 스택에 빈 전역 실행 컨텍스트 푸쉬

    2.2 = 전역 렉시컬 환경 생성 후 실행 컨텍스트에 바인딩

     

    2.2.1 =

    전역 환경 레코드는 var 와 const, let 을 구분하여 관리하기 위해 객체 환경 레코드와 선언적 환경 레코드로 구분되어 구성되어 있다.

    Object Environment Record
    Binding Object next -> window (x = undefined, foo()<function object>, ...)
    Declarative Environment Record
    y uninitialized

     

    2.2.2 =

    전역 환경 레코드에는 2.2.1의 두 레코드에 이어서, [[GlobalThisValue]] 라는 내부 슬롯에 this 를 바인딩 한다 ! 즉, 전역 코드에서 this 를 참조하면 바인딩 되어있는 전역 객체가 반환된다는 것.

     

    2.2.3 = 외부 참조 (이 경우에는 전역이라 없음)

     

    위의 과정까지 마친 전역 렉시컬 환경의 모습은 다음과 같다.

     

    Global Lexical Environment
    Global Environment Record Object Environment Record
    Declarative Environment Record
    [[GlobalThisValue]] (this binding) -> window (...)
    Outer Lexical Environment Reference null

    위와 같이 생성이 모두 완료된 채 평가가 모두 끝나면, 이제 전역 코드가 순차적으로 실행되서 값이 할당되고 함수가 호출된다.

     

    여기서 눈치 챌 수 있는 포인트 ! 이 스코프 안에 x와 y와 foo가 선언되어 있는데, 이 스코프 바깥에 똑같은 이름의 변수나 함수가 선언되어 있는지는 신경쓰지 않는다. 따라서, 스코프가 다르다면 동일한 이름의 식별자가 있어도 상관이 없다 !

    (물론 var x는 전역 객체에 선언되어 있고, 다른 스코프에서도 this 바인딩으로 참조하는 식으로 쓸 거니까 x는 1개만 가능하다.)

     

    위와 같은 흐름으로 함수 코드도 평가되고 실행된다. 다만 차이점은, 함수 환경 레코드는 [[ThisValue]] 슬롯에 this 가 바인딩 되는데, 여기에 바인딩 될 객체는 함수 호출 방식에 따라 결정된다는 것. 그리고 외부 참조가 전역 렉시컬 환경에서는 null 이었지만, 내부 함수에서는 [[Environment]] 슬롯에 상위 스코프를 저장한다는 것 ! (클로저를 이해할 수 있는 중요한 단서)

     

    여기까지 이해했으면 이제 클로저를 이해하는 데 불편함이 없을 거라 생각한다.

    댓글

Designed by Tistory.