Javascript

[코어 자바스크립트] 실행 컨텍스트

minjaem 2024. 2. 22. 21:30

 

실행 컨텍스트

실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다. 실행 컨텍스트 객체는 활성화 되는 시점VariableEnvironment, LexicalEnvironment, ThisBinding의 세 가지 정보를 수집한다.

 

동일한 환경에 있는 코드들을 실행할 떄 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜 스택에 쌓아 올렸다가 가장 위에 쌓여있는 컨텍스트와 관련있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.

 

처음 자바스크립트를 실행하는 순간, 전역 컨텍스트가 콜 스택에 담긴다. 전역 컨텍스트라는 개념은 일반적인 실행 컨텍스트와 특별히 다를 것이 없으나 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 자동으로 활성화된다. 

이후, A 함수를 “호출”하게 되면 자바스크립트 엔진은 A 실행 컨텍스트를 생성한 후에 콜 스택에 담는다. 모든 실행 컨텍스트를 콜 스택에 담았으면 LIFO(Last In First Out)순서로 실행 컨텍스트는 콜 스택에서 제거된다.

 

스택 구조를 잘 생각해보면 한 실행 컨텍스트가 콜 스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점임을 알 수 있다. 기존의 컨텍스트는 새로 쌓인 컨텍스트보다 아래에 위치할 수 밖에 없기 때문이다. 이렇게 어떤 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 “해당 컨텍스트에 관련된 코드들을 실행하는데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장”한다. 이 객체는 자바스크립트 엔진이 활용할 목적으로 생성할 뿐 개발자가 코드를 통해 확인할 순 없다. 여기에 담기는 정보는 다음과 같다.

 

VariableEnvironment: 현재 컨텍스트 내의 식별자들에 대한 정보와 외부 환경 정보. 선언 시점의 LexicalEnvironment의 스냅샷으로 변경사항은 반영되지 않는다.

LexicalEnvironment: 처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨.

ThisBinding: 식별자가 바라봐야 할 대상 객체.

 

VariableEnvironment

VariableEnvironment에 담기는 내용은 LexicalEnvironment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 다르다. 실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LexicalEnvironment를 만들고, 그 이후에는 LexicalEnvironment를 주로 활용하게 된다.

  VariableEnvironment와 LexicalEnvironment의 내부는 environmentRecord와 outer-EnvironmentReference로 구성되어 있다. 초기화 과정 중에는 사실상 완전히 동일하고 이후 코드 진행에 따라 서로 달라지게 될 것이므로 자세한 내용은 LexicalEnvironment를 통해 살펴보자.

LexicalEnvironment

environmentRecord와 호이스팅

environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자(매개변수의 이름), 선언한 함수가 있을 경우 그 함수 자체(함수 선언), var로 선언된 변수의 식별자(변수명) 등이 해당 된다.

 

호이스팅 규칙

호이스팅이란 '끌어올리다.'라는 의미의 hoist에 ing를 붙여 만든 동명사로, 코드 해석을 좀 더 수월하게 하기 위해 environmentRecord의 수집 과정을 추상화한 개념으로, 실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 '끌어올린다'고 해석하는 것이다.

 

매개변수와 변수에 대한 호이스팅

function a(x) { // 수집 대상 1(매개 변수)
    console.log(x); // (1)
    var x; // 수집 대상 2(변수 선언)
    console.log(x); // (2)
    var x = 2; // 수집 대상 3(변수 선언)
    console.log(x); // 3
}

a(1);

위 예제를 보면 (1), (2), (3)에서 어떤 값들이 출력될지 예상해보자. 

(1)에는 1, (2)에는 undefined, (3)에는 2라는 값이 출력될 것 같지 않은가?

자, 이제 정답을 보자.

호이스팅을 마친 상태로 코드로 한번 표현해보았다.

function a() {
    var x; // 수집 대상 1의 변수 선언 부분
    var x; // 수집 대상 2의 변수 선언 부분
    var x; // 수집 대상 3의 변수 선언 부분
    
    x = 1; // 수집 대상 1의 할당 부분
    console.log(x); // (1)
    console.log(x); // (2)
    x = 2; // 수집 대상 3의 할당 부분
    console.log(x) // (3)
}

a(1)

정답은 예상과 달리 (1)은 1, (2)는 1, (3)은 2이다.

위와 같이, environmentRecord는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 어떤 값이 할당될 것인지는 관심이 없다. 따라서, 변수를 호이스팅할 때 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 남겨둔다. 매개 변수의 경우도 마찬가지이다.

 

함수 선언에 대한 호이스팅

function a() {
    console.log(b); // (1)
    var b = 'bbb'; // 수집 대상 1(변수 선언)
    console.log(b); // (2)
    function b() { // 수집 대상 2(함수 선언)
    	console.log(b); // (3)
    }
}

a();

이번에도 출력결과를 예상해보자.

(1)에는 undefined, (2)에는 'bbb', (3)에는 b 함수가 출력될 것만 같다. 실제로도 그럴까?

a 함수를 실행하는 순간 a 함수의 실행 컨텍스트가 생성된다. 이때 변수명과 함수 선언의 정보를 위로 끌어 올린다. 변수는 선언부와 할당부로 나누어 선언부만 끌어올리는 반면 함수 선언은 함수 전체를 끌어 올린다. 수집대상 1과 2를 순서대로 끌어올리고 나면 다음과 같은 형태로 변환된다.

function a() {
    var b; // 수집 대상 1. 변수는 선언부만 끌어올린다.
    function b() {} // 수집 대상 2. 함수 선언은 전체를 끌어올린다.
    
    console.log(b); // (1)
    b = 'bbb'; // 변수의 할당부는 원래 자리에 남겨둔다.
    console.log(b) // (2)
    console.log(b) // (3)
}

a()

예상과 달리, 실제로 (1)에는 b함수, (2)에는 'bbb', (3)에는 'bbb'라는 결과가 출력됐다.

 

함수 선언문과 함수 표현식

// 함수를 정의하는 세 가지 방식

function a() {} // 함수 선언문
a() // 실행 OK

var b = function() {} // 익명 함수 표현식
b() // 실행 OK

var c = function d() {} // 기명 함수 표현식
c() // 실행 OK
d() // 에러!

함수 선언문은 전체를 호이스팅 하는 반면, 함수 표현식은 선언된 변수의 선언부만 끌어올린다.

이것이 함수 선언문과 함수 표현식의 극적인 차이이다.

 

스코프, 스코프 체인, outerEnvironmentReference

스코프(Scope)

스코프란 식별자에 대한 유효범위이다. ES5 이전 까지는 전역 공간을 제외하면 오직 함수에 의해서만 스코프가 생성됐으나, ES6 이후로는 let과 const, class, strict mode에서의 함수 선언 등의 범위에서만 블록에 의해서도 스코프가 생성된다.

 

스코프 체인(Scope Chain)

식별자의 유효범위를 안에서부터 바깥으로 차례대로 검색해 나가는 것이다. 이를 가능케 하는 것이 바로 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference이다.

 

전역변수와 지역변수

전역 공간에서 선언한 변수는 전역 변수이고, 함수 내부에서 선언한 변수는 지역 변수이다.

 

ThisBinding

실행 컨텍스트를 활성화하는 당시에 지정된 this가 저장된다. 함수를 호출하는 방법에 따라 그 값이 달라지는데, 지정되지 않은 경우에는 전역 객체가 저장된다.

 

* 정리하며 다시 스스로 생각해볼 질문

  1. 실행컨텍스트란 무엇인가요?
  2. 호이스팅이란 무엇인가요?
    • 호이스팅의 개념
    • 매개변수 / 변수의 호이스팅과 함수의 호이스팅에 대한 차이 및 개념
  3. 스코프와 스코프 체인이란 무엇인가요?