본문 바로가기

프로그래밍/JavaScript(JS)

자바스크립트(JavaScript) - Execution Context(실행 문맥)과 코드 실행 및 내부 동작 원리

ECMAScript2015(ES6)을 중점으로 다루고 있습니다. 참고하시기 바랍니다. 

 

글에 오류가 있을 수도 있으니 알려주시면 감사하겠습니다.

 

실행 문맥(Execution Context)

실행 문맥은 실행이 가능한 코드들이 평가 또는 실행되고 관리되는 영역이며 코드에 대한 정보들이 저장되어있다.

여기서 평가는 코드가 전역 변수인지 지역 변수인지 변수의 값이 무엇인지 범위는 어디까지인지 코드를 상황에 맞게 파악하는 것이라고 보면 될 것 같다.

 

Execution Context(실행 문맥)가 가지고 있는 구성요소(ES5.1)

렉시컬 환경(LexicalEnvironment)

변수 환경(VariableEnvironment)

디스 바인딩(ThisBinding)

 

LexicalEnvironment은 자바스크립트 코드를 실행하기 위해 함수, 블록의 유효 범위 안에 있는 식별자(변수)와 결괏값을 가지는 환경

VariableEnvironment은 var 키워드로 선언되거나 function키워드로 선언된 변수, 함수가 저장되는 환경

ThisBinding은 함수를 호출한 객체의 참조가 저장됩니다.

 

Execution Context(실행 문맥)가 가지고 있는 구성요소(ES6)

렉시컬 환경(LexicalEnvironment)

변수 환경(VariableEnvironment)

 

LexicalEnvironment은 자바스크립트 코드를 실행하기 위해 함수, 블록의 유효 범위 안에 있는 식별자(변수이름)와 결괏값을 가지는 환경 간단하게 let, const, fcuntion으로 선언된 변수와 함수가 저장되는 환경

VariableEnvironment는 var 키워드로 선언된 변수가 저장되는 환경

 

ES6(2015) 스펙에서는 실행 문맥에 디스 바인딩(ThisBinding)이 없습니다. ES6에서는 this를 호출하면 현재 LexcialEnvironment가 반환되어 현재 LexcialEnvironment에 있는 GetThisBinding()를 호출하여 얻습니다.

 

렉시컬 환경, 변수 환경의 구성요소

환경 레코드(Environment Record)

외부 렉시컬 환경 참조(Outer Lexical Environment Reference)

 

  • 환경 레코드는 유효 범위안에 있는 식별자를 기록하고 실행하는 영역 식별자와 결괏값을 최종적으로 저장되는 곳이다.
  •  
  • 외부 렉시컬 환경 참조는 함수 또는 변수가 속한 렉시컬 환경이 저장되며 중첩이 되어있는 함수나 지역변수가 아닌 전역 변수, 바깥쪽에 선언된 변수를 사용할 때 외부 렉시컬 환경 참조에 있는 렉시컬 환경에 가서 필요한 것을 찾아 사용합니다.

 

function a()
{
    var b = "b";
    function c()
    {
    	// 또잉?
    }
}

위 코드가 있을 때 c함수의 렉시컬 환경에 있는 외부 렉시컬 환경 참조에는 a의 렉시컬 환경이 저장이 된다. 따라서 c에서 b의 변수를 사용하고 싶을 때 외부 렉시컬 환경 참조에 있는 렉시컬 환경으로 이동해 b의 변수를 찾아 사용하는 것이 됩니다.

 

환경 레코드의 종류

환경 레코드의 종류에는 ES6기준 5개가 있습니다.

 

환경 레코드는 렉시컬 환경에서 식별자와 결괏값이 저장되는 영역이며 저장되는 값의 유형의 따라서 사용되는 것이 다릅니다.

 

  • 선언적 환경 레코드(Declarative Environment Records) - 함수와 변수, let, const, class, module, import, catch 문의 식별자와 결괏값이 저장되는 곳
  • 객체 환경 레코드(Object Environment Records) - 식별자 이름으로 bindObject에 바인딩합니다. with문 같은 것을 사용할 때 해당하는 식별자 이름들이 bindObject에 연결됩니다.
  • 전역 환경 레코드(Global Environment Records) - 맨 처음 만들어지는 환경 레코드입니다. 전역 환경 레코드에는 선언적 환경 레코드, 객체 환경 레코드를 복합적으로 사용하며 객체 환경 레코드에 bindObject에 전역 객체를 바인딩하고 있습니다.
  • 함수 환경 레코드(Function Environment Records) - 함수가 호출되면 만들어지는 환경 레코드입니다. 함수 안에 있는 변수와 함수들을 관리합니다.
  • 모듈 환경 레코드(Module Environment Records) - 모듈에 대한 환경 레코드입니다. 모듈에 있는 변수와 함수가 바인딩됩니다.

 

전역 환경과 전역 객체 생성

전역 환경은 말 그대로 스크립트가 시작되는 처음 영역이다. 스크립트가 시작되면 Execution Context가 생성되고 전역 환경 레코드가 만들어집니다.

 

전역 환경 레코드에는 선언적 환경 레코드(Declarative Environment Records), 객체 환경 레코드(Object Environment Records)와 VarName이라는 리스트 타입에 필드가 있습니다. 추가적으로 VarName 필드는 var로 선언된 변수, 함수의 이름이 저장됩니다.

 

전역 객체 생성과정을 간단하게 확인해보겠습니다.

 

자바스크립트가 처음 실행되면 실행 문맥이 만들어지면서 스크립트의 코드 영역(Realm) 이 생성한 후 전역 객체(브라우저는 window 객체)를 생성하고 전역 환경 레코드(Global Environment Records)가 생성되며 bindingObject에 전역 객체를 바인딩합니다. 이때 built-in 객체의 속성도 포함됩니다.

외부 렉시컬 환경 참조는 전역 환경 외부에 다른 렉시컬 환경이 없기 때문에 null이 됩니다.

 

그 후 코드의 Execution Context(실행 문맥)이 만들어지면서 LexicalEnvironment, VariableEnvironment에 전역 환경 레코드가 바인딩됩니다.

 

이렇게 만들어진 전역 환경은 만들어진 Global Execution Context(전역 실행 문맥)에 들어가 있습니다.

 

전역 실행 문맥을 표현하면 아래와 같습니다.

ExecutionContext = {
    LexicalEnvironment : globalEnv
    VariableEnvironment : globalEnv
}

//Lexical Environment
globalEnv = {
    EnvironmentRecord : globalRec
    OuterLexicalEnvironmentReference : null
}

objRec = {
    //ObjectEnvironmentRecord
    bindingObejct : Window
}

dclRec = {
    //DeclarativeEnvironmentRecord
}

globalRec = {
    [[ObjectReocrd]] : objRec
    [[DeclarativeRecord]] : dclRec
    [[VarNames]] : []
}

 

호이스팅 및 전역 변수

Execution Context가 만들어지고 스크립트 평가가 시작되면 변수 선언과 함수 선언을 찾아 바인딩합니다.

이때 바인딩되면서 var값은 undefined로 초기화되고 let, const로 선언된 변수는 이름만 바인딩이 되고 값은 초기화가 되지 않은 상태가 됩니다.

 

바로 이때가 호이스팅되어진 상태입니다. 그래서 스크립트를 평가할 때 미리 변수와 함수를 찾아 모든 선언이 레코드에 바인딩되고 초기화가 되고 난 후 실제 코드 평가 때 변수나 함수를 사용해도 에러가 나지 않는 것입니다.

 

다만 let, const으로 선언된 변수도 호이스팅이 되지만 값이 초기화가 되지 않아 접근이 불가능하여 에러가 나타는 것입니다.

 

let a = 0;
var b = 1;
function ab(){ var c = 2;};

console.log(window.a) // -> undefined
console.log(window.b) // -> 1
console.log(window.ab) // -> ab() { var c = 2; }

자바스크립트 코드를 보면 var문과 function으로 전역으로 선언된 변수와 함수는 모두 window(전역 객체)에 포함되어 있습니다. 전역 객체에서 코드의 탑 레벨에 선언된 var 변수나 함수는 코드 평가 때 전역 환경 레코드에 있는 bindingObject 전역 객체에 추가가 되기 때문에 window객체의 프로퍼티로 접근이 가능합니다.

 

프로그램 평가 및 실행

이번에는 간단한 자바스크립트 코드를 확인해보겠습니다.

let a = 0;
var b = 1;
function ab()
{
    //
};

해당 코드가 있습니다. 해당 코드의 실행 문맥(Execution Context)을 확인해보겠습니다.

ExecutionContext = {
    LexicalEnvironment : globalEnv
    VariableEnvironment : globalEnv
}

globalEnv = {
    //Lexical Environment
    EnvironmentRecord : globalRec
    OuterLexicalEnvironmentReference : null
}

objRec = {
    //ObjectEnvironmentRecord
    bindingObejct : Window // b add, ab() add
}

dclRec = {
    //DeclarativeEnvironmentRecord
    a:uninitalized
}

globalRec = {
    //GlobalEnvironment
    [[ObjectReocrd]] : objRec
    [[DeclarativeRecord]] : dclRec
    [[VarNames]] : ["b", "ab"]
}

코드 영역이 만들어지고 스크립트 평가 시작은 전역 실행 문맥(Global Execution Context)으로 시작이 됩니다.(원래는 전역 실행 문맥이라는 것이 따로 없습니다. 설명상 쓰는 것입니다.)

 

전역 환경이 만들어지고 코드에 대한 실행 문맥이 만들어지고 환경 레코드에 있는 객체 환경 레코드는 var, 함수가 바인딩되고 선언적 환경 레코드는 var, 함수를 제외한 것들이 바인딩됩니다. 대표적으로 let, const로 선언된 변수가 있겠습니다.

 

참고로 전역 환경 레코드에서만 var문으로 선언된 변수, 함수들이 객체 환경 레코드에 바인딩되는 것이고 함수 안쪽에 있는 var 키워드로 선언된 변수, 함수들도 선언적 환경 레코드에 저장된다는 점입니다.

 

따라서 let으로 선언된 a변수는 선언적 환경 레코드에 바인딩되고 var로 선언된 b변수는 객체 환경 레코드에 ab 함수도 객체 환경 레코드에 바인딩됩니다. 함수는 함수 객체가 만들어지고 함수 객체가 바인딩 됩니다.

 

지금까지 Global Execution Context에 있는 전역 환경 레코드가 만들어지는 과정이었습니다. 

 

환경 레코드에 모두 선언되고 초기화되면 실제 코드 평가가 시작이 됩니다. 현재 모든 값은 undefined, uninitalized, 함수 객체입니다.

 

이제 한줄한줄 실제 코드 평가가 시작되면 이때부터 함수를 호출하거나 지정해준 값들이 변수에 들어갑니다.

 

따라서 코드가 한줄한줄 실행되면 아래와 같이 변하게 됩니다.

let a = 0;
var b = 1;
function ab()
{
    //emtpy
};
ExecutionContext = {
    LexicalEnvironment : globalEnv
    VariableEnvironment : globalEnv
}

globalEnv = {
    //Lexical Environment
    EnvironmentRecord : globalRec
    OuterLexicalEnvironmentReference : null
}

objRec = {
    //ObjectEnvironmentRecord
    bindingObejct : Window // b = 1, ab()
}

dclRec = {
    //DeclarativeEnvironmentRecord
    a:0
}

globalRec = {
    //GlobalEnvironment
    [[ObjectReocrd]] : objRec
    [[DeclarativeRecord]] : dclRec
    [[VarNames]] : ["b", "ab"]
}

 

이번엔 함수가 호출되었을 때 만들어지는 Execution Context를 확인해보도록 하겠습니다.

함수 호출과 새로운 Execution Context

코드가 실행되면서 함수 호출을 만나면 새로운 실행 문맥을 생성합니다. 이때 Excution Contxt Stack에 새로 만들어진 실행 문맥을 추가합니다. Excution Context Stack은 말 그대로 스택 형식을 가지고 있습니다.

 

실행 문맥이 생성되면 Execution Context Stack에 푸시해야 작업이 진행됩니다.

 

차례차례 푸시되면서 작업이 쌓이게 되고 이 작업을 위에서 하나씩 차례차례 작업을 진행하고 모든 작업이 진행되면 스택에서 제거됩니다.

 

아까와 같은 스크립트가 있고 마지막에 ab함수를 호출했습니다. 

let a = 0;
var b = 1;
function ab()
{
    console.log(c) // -> undefined
    var c = 2;
    console.log(c) // -> 2
    console.log(d) // -> Reference Error
    let d = 3;
};

ab();

이제 ab함수가 호출되었을 때 Execution Context가 만들어지고 함수의 정보를 저장할 렉시컬 환경이 생성되고 이 렉시컬 환경에는 Function Environment Record가 생성됩니다. 이 함수 환경 레코드에는 ab함수에 대한 기본적인 함수 환경 레코드 정보가 저장됩니다.

 

이후 스택에 푸시되며 푸시된 Execution Context를 실행합니다.

ExecutionContext = {
    LexicalEnvironment : env
    VariableEnvironment : env
}

env = {
    EnvironmentRecord : envRec
    OuterLexicalEnvironmentReference : globalEnv
}

envRec = {
    //FunctionEnvironmentRecord
    [[thisValue]] : "globalObejct",
    [[thisBindingStatus]] : uninitialized,
    [[FunctionObject]] : ab,
    [[HomeObject]] : "undefined",
    [[newTarget]] : "undefined"
}

기본적인 설정이 끝나면 함수에 선언된 변수와 또 다른 함수의 정보가 바인딩됩니다. 

ExecutionContext = {
    LexicalEnvironment : lexEnv
    VariableEnvironment : env
}

// 파라미터가 기본값이 있는경우 기존 env를 사용하지 않고 새로운
// DeclarativeEnvironment를 varEnv에 생성(외부 환경 참조는 env) 후 변수 바인딩 후 
// VariableEnvironment 에 varEnv 바인딩
// 아니라면 기존 envRec에 변수를 바인딩하고 아래처럼 varEnv에 env 저장

env = {
    EnvironmentRecord : envRec
    OuterLexicalEnvironmentReference : globalEnv
}

envRec = {
    //FunctionEnvironmentRecord
    [[thisValue]] : "globalObejct",
    [[thisBindingStatus]] : "uninitialized",
    [[FunctionObject]] : ab,
    [[HomeObject]] : "undefined",
    [[newTarget]] : "undefined",
    c : "undefined"
}

// 환경 백업
// varEnvRec = envRec
// varEnv = env

// strict = true 일때는 밑에 새로운 환경을 만들지 않고 lexEnv이름으로 varEnv를 그대로 사용
// strict = true : lexEnv = varEnv

// lexEvn를 LexicalEnvironment로 설정

lexEnv = {
    EnvironmentRecord : lexEnvRec
    OuterLexcialEnvironmentReference : varEnv
}

lexEnvRec = {
    //DeclarativeEnvironmentRecord
    d : uninitalized
}

// 함수는 함수 생성 후 varEnvRec에 바인딩

var로 선언된 변수가 저장될 때 호출된 함수의 매개변수가 기본 값이 있다면 내부적으로 현재 함수 환경(env)을 외부 환경 참조로 하여 varEnv라는 곳에 새로운 선언 환경을 만듭니다. 그 후 varEnv에 있는 레코드 환경에 변수들을 바인딩하고 varEnv를 VariableEnvironment로 설정합니다.

 

만약 기본 값이 없다면 기존 함수 환경에 있는 환경 레코드에 변수를 바인딩하고 함수 환경 레코드를 varEnvRec라는 곳에 저장하고 현재 함수 환경을 varEnv에 저장합니다. 

 

다음으로 let이나 const같이 var를 제외한 선언들을 바인딩합니다.

 

만약 strict이 true라면 새로운 렉시컬 환경을 만들지 않고 원래 있던 varEnv를 lexEnv에 저장합니다.

 

만약 strict가 false라면 lexEnv에 외부 환경 참조가 varEnv인 새로운 선언 환경 레코드를 만듭니다.

 

그 후 실행 문맥에 있는 LexcialEnvironment에 lexEnv를 바인딩합니다. 그 후 lexEnv에 변수들을 바인딩합니다.

 

마지막으로 함수들을 생성하고 varEnvRec에 있는 레코드에 함수들을 바인딩합니다.

 

모든 변수들과 함수가 바인딩되고 나면 실제 코드가 실행이 됩니다.

let a = 0;
var b = 1;
function ab()
{
    console.log(c) // -> undefined
    var c = 2;
    console.log(c) // -> 2
    console.log(d) // -> Reference Error
    let d = 3;
};

ab();
ExecutionContext = {
    LexicalEnvironment : lexEnv
    VariableEnvironment : env
}

env = {
    EnvironmentRecord : envRec
    OuterLexicalEnvironmentReference : globalEnv
}

envRec = {
    //FunctionEnvironmentRecord
    [[thisValue]] : "globalObejct",
    [[thisBindingStatus]] : "uninitialized",
    [[FunctionObject]] : ab,
    [[HomeObject]] : "undefined",
    [[newTarget]] : "undefined",
    c : 2
}

// 환경 백업
// varEnvRec = envRec
// varEnv = env

lexEnv = {
    EnvironmentRecord : lexEnvRec
    OuterLexcialEnvironmentReference : varEnv
}

lexEnvRec = {
    //DeclarativeEnvironmentRecord
    d : 3
}

// 함수는 함수 생성 후 varEnvRec에 바인딩

전역 환경처럼 한줄한줄 실행이 되면 지정한 값이 들어가거나 함수가 호출이 됩니다.

 

이것으로 실행 문맥과 코드가 평가되는 과정 그리고 실행되는 과정과 내부 정보를 살짝 알아보았습니다.

 

참고

https://262.ecma-international.org/6.0/#sec-executable-code-and-execution-contexts

 

ECMAScript 2015 Language Specification – ECMA-262 6th Edition

5.1.1 Context-Free Grammars A context-free grammar consists of a number of productions. Each production has an abstract symbol called a nonterminal as its left-hand side, and a sequence of zero or more nonterminal and terminal symbols as its right-hand sid

262.ecma-international.org