2 실행 컨텍스트

실행 컨텍스트

실행 컨텍스트 : 실행할 코드에 제공할 환경 정보들을 담은 객체

동일한 환경에 있는 코드들을 실행할 때, 그 실행에 필요한 환경정보들을 모아서 컨텍스트를 생성한다. 이 컨텍스트를 콜스택에 쌓아서 가장 위에 있는 컨텍스트와 관련된 코드를 실행하는 구조이다. 이런 구조로 전체 코드의 실행 순서와 환경을 보장할 수 있다.

실행 컨텍스트가 구성되는 방법은 총 3가지 :

  1. 전역 공간
  2. eval 함수 (악마라는 소문이 있다.)
  3. 함수 실행

전역 공간은 코드가 실행될 때 자동으로 생성되는 것이고, eval 함수는 웬만하면 사용하지 않는 것이 좋다는 의견이 많으므로 일반적으로 우리가 실행 컨텍스트를 만들 수 있는 상황은 주로 함수 실행이다.

function outer() {
	function inner() {
		console.log("여기는 inner.");
	}
	console.log("outer 시작");
	inner();
	console.log("outer 종료");
}
console.log("코드 시작합니다.");
outer();
console.log("코드 종료합니다.");

실행 컨텍스트와 관련된 흐름을 간단히 정리해보면 :

  1. JS 코드를 실행하는 순간 전역 컨텍스트가 콜스택에 담긴다.

  2. 코드를 실행하다가 outer 함수를 호출하면 outer 함수의 실행과 관련된 환경정보를 수집해서 outer 함수 실행 컨텍스트가 생성되어 콜스택에 담긴다.

    ⇒ 콜스택 최상단이 outer 함수이므로 전역 실행을 중단하고 outer 함수를 실행한다.

  3. outer 함수 코드를 실행하다가 inner 함수를 호출하면 inner 함수의 실행과 관련된 환경정보를 수집해서 inner 함수 실행 컨텍스트를 생성해서 콜스택에 담는다.

    ⇒ 콜스택 최상단이 inner 함수이므로 outer 함수 실행을 중단하고 inner 함수를 실행한다.

  4. inner 함수가 종료되면 콜스택에서 inner 함수 실행 컨텍스트를 제거한다.

    ⇒ 콜스택 최상단이 outer 함수이므로 outer 함수로 돌아가 이전 중단 시점부터 다시 실행한다.

  5. outer 함수가 종료되면 콜스택에서 outer 함수 실행 컨텍스트를 제거한다.

    ⇒ 콜스택 최상단이 전역이므로 전역 코드로 돌아가 이전 중단 시점부터 다시 실행한다.

  6. 전역 코드가 종료되면 콜스택에서 전역 실행 컨텍스트를 제거한다.

    ⇒ 콜스택이 비어 최종 종료된다.

실행 컨텍스트에 담기는 실행 정보들은 3가지이다.

  1. VariableEnvironment
  2. LexicalEnvironment
  3. ThisBinding

VariableEnvironment

VariableEnvironment는 LexicalEnvironment의 초깃값이다.

처음 실행 컨텍스트가 생성될 때 VariableEnvironment가 먼저 생성되고, 이걸 복사해서 LexicalEnvironment를 생성한 다음에 VariableEnvironment는 그대로 두고 LexicalEnvironment를 코드 진행에 따라 변경시킨다. 초기 스냅샷이라고 생각하면 될 듯하다.

구성은 아래와 같은데, LexicalEnvironment와 동일하기 때문에 LexicalEnvironment를 통해 더 자세히 살펴본다.

  • environmentRecord
  • outerEnvironmentReference

LexicalEnvironment

보통 어휘적 환경이라고 번역하는데, 이 책에서는 사전적인 환경이라고 번역하면 이해하기 쉽다고 설명한다. (물론 용어는 약속이기 때문에 LexicalEnvironment 그대로 알아두는 것이 중요하다.)

마치 사전처럼 어떤 식별자들이 있고, 외부 식별자는 어디를 통해서 찾으면 되는지를 저장해둔 것이다. 앞서 봤던 것처럼 LexicalEnvironment는 environmentRecord와 outerEnvironmentReference로 구성되어 있다. (단어 길이가 너무 길다…)

environmentRecord

현재 컨텍스트와 관련된 식별자들이 저장되어 있다. 이 식별자에는 매개변수, 함수, 변수들이 포함되고, 이 정보들을 코드를 돌면서 순서대로 수집해서 저장한다.

전역 실행 컨텍스트는 변수 객체 대신 전역 객체를 사용한다. (window, global, … 같은 호스트 객체)

중요한 것은 일단 모든 식별자를 수집한 뒤에 실제 코드를 실행한다는 것이다. 따라서 실제 코드를 실행할 때에 이미 코드 내의 모든 식별자를 알고 있는 상태가 되는데, 이로 인해서 발생하는 현상이 호이스팅이다.

함수 선언문과 함수 표현식

  • 함수 선언문 : 정의만 존재하고 할당은 하지 않는 것

    function a () { / * … * /}

  • 함수 표현식 : 함수를 다른 변수에 할당하는 것

    var b = function () { / * … * /}

    var c = function d () { / * … * /}

기명함수 표현식에서 기명함수의 이름으로는 함수 호출이 불가능하다. c 함수 내부에서 재귀 호출을 하는 경우에는 cd 모두를 쓸 수 있긴 한데, 아주 특별한 경우가 아니라면 c로 호출하는 것이 좀 더 자연스러울 것 같다. (저자분의 생각도 그렇고, 나도 그렇게 생각한다.)

그럼 이 이름은 어디에 쓰이냐면 보통 디버깅 할 때 활용하는 함수 객체의 name 프로퍼티의 값으로 쓰였다. 하지만 요즘 브라우저들은 익명함수 표현식을 쓰더라도 변수명을 name 프로퍼티에 넣어주기 때문에 이 목적으로도 굳이 쓸 필요 없다.

둘은 호이스팅 결과에 차이가 있다. 함수 선언문은 선언부까지 모두 호이스팅되고 함수 표현식은 식별자만 호이스팅된다는 것이다! 선언부까지 모두 호이스팅되는 현상은 예상치 못한 결과를 낼 수 있어서 좀 위험할 수 있다.

극단적 예시 :

function func() { console.log("여기선 이렇게") };
func();
function func() { console.log("여기부턴 저렇게") };
func();

왠지 “여기선 이렇게” - “여기부턴 저렇게” 순으로 출력될 것 같지만 “여기부턴 저렇게” 가 두 번 출력된다. 동일한 식별자의 경우에는 가장 마지막에 선언된 것으로 값이 덮어씌워지기 때문이다. (override) 함수 표현식이었다면 할당이 코드를 실행하면서 이뤄지기 때문에 의도한 대로 작동했을 것이다. (하지만 함수 표현식을 쓴다고 해도 웬만하면 동일한 식별자를 갖는 함수를 중복 선언하지 않는 것이 좋다.)

outerEnvironmentReference

스코프란 식별자의 유효범위를 의미한다. 그리고 스코프를 안에서부터 밖으로 차례로 검색하는 것을 스코프 체인이라 한다. 이 스코프 체인을 가능하게 하는 것이 바로 outerEnvironmentReference 덕분인데, 여기에는 함수 선언 당시의 LexicalEnvironment가 저장된다.

스코프 체인은 연결리스트처럼 연결되어 있다. 연결리스트를 탐색하듯이 일치하는 식별자를 찾을 때까지 상위 스코프로 이동한다. 그리고 가장 먼저 발견된 식별자를 사용한다. 따라서 상위 스코프의 동일한 식별자에는 접근이 불가능해지는데, 이걸 변수 은닉화라고 한다.

“함수 선언 당시”의 LexicalEnvironment라는 말이 좀 헷갈리는데 코드로 봤을 때 좀 더 이해가 잘됐다.

var a = "전역";
function outer() {
  var a = "outer";
  function inner() {
    console.log(a);
  }
  return inner; // 선언 시점: outer의 LexicalEnvironment
}
var fn = outer();
fn(); // 호출 시점: 전역 — 그러나 출력은 "outer"

함수 선언 시에 활성화된 LexicalEnvironment를 함수 객체의 [[Environment]]에 저장한다.

→ 함수 호출

→ 주변의 환경정보 모아서 실행 컨텍스트 생성. 그 과정에서 outerEnvironmentReference에 함수 객체의 [[Environment]] 저장.

⇒ 따라서 선언 당시의 LexicalEnvironment가 저장됨

으로 이해했다.

ThisBinding

this로 지정된 객체가 저장된다. this에 대한 자세한 내용은 3장에서 다룬다.