Skip to content

Instantly share code, notes, and snippets.

@snaag
Last active January 12, 2023 00:57
Show Gist options
  • Save snaag/de92e29268429ada9da686125ac937e1 to your computer and use it in GitHub Desktop.
Save snaag/de92e29268429ada9da686125ac937e1 to your computer and use it in GitHub Desktop.
Execution Context

들어가며

지난 JavaScript, Front-End 발표 주제는 this 였지만, 공부하다 보니 실행 컨텍스트에 대한 내용이 선행되야 할 것 같아 실행 컨텍스트에 대하여 발표를 하게 되었다.

여러 자료와 책을 참고하며 공부를 하고 있음에도 내용이 잘 와닿지 않아 참고1참고2, 참고3, 참고4, 책 인사이드 자바스크립트, Poiema Web을 참고하여 번역을 해보고자 한다. 추가적으로 내가 여러 자료를 찾아보면서 알게 된 내용들도 덧붙일 것이다.

실행 컨텍스트, Execution Context(이하 EC) 라는 개념은 나에겐 낯설었기에 나와 같은 사람들이 있을 것 같아 흔히들 아는 콜스택 을 시작으로 글을 써보도록 하겠다.

Call Stack과 EC

img

var x = 'xxx';

function foo () {
  var y = 'yyy';

  function bar () {
    var z = 'zzz';
    console.log(x + y + z);
  }
  bar();
}

foo();

저 그림은 EC가 들어있는 EC Stack이라고 한다. 그런데 콜스택이랑 굉장히 비슷한 것 같은데, 둘이 다른건가?

같은거야. StackOverflow의 한 유저가 같은 질문에 둘은 같은것을 지칭하는 서로 다른 이름이라고 말해주는 자료를 한번 봐봐.

즉 콜스택 안에 들어가는 것은 EC를 말한다. 그리고 컴파일의 단위이다. 그리고 각 EC는 독립적이다.

그림 및 코드 참고

Execution Context

EC? 그게 뭐야?

EC는 실행 가능한 자바스크립트 코드 블록이 실행되는 환경 을 말한다.

-인사이드 자바스크립트 140p

실행 컨텍스트는 Javascript 코드가 evaluate 되고, execute 되는 환경에 대한 추상적인 개념입니다.

-참고1

즉 어떤 환경(에 대한 추상적인 개념) 이라는 것인데, 이는 코드가 잘 작동할 수 있도록 변수와 함수(모든 객체)가 가진 값을 알고 이에 대한 환경을 구성하는 것을 말한다. (아래에 보다 구체적으로 다룰 것이다)

EC 의 3가지 종류

  1. Global Execution Context (전역 실행 컨텍스트)
  2. Functional Execution Context (함수 실행 컨텍스트)
  3. Eval Function Execution Context (eval 실행 컨텍스트)

Global Execution Context (전역 실행 컨텍스트)

가장 먼저 콜스택에 올라가는 EC이다. 전역 EC는 일반적인 다른 EC들과 달리 arguments 객체가 없으며, 전역 객체 하나만을 포함하는 스코프 체인this 가 있다.

그리고 전역 EC는 <script /> 태그를 마주치면, 생성된다.

Functional Execution Context (함수 실행 컨텍스트)

함수가 호출될 때마다 생성되는 EC로 arguments 와 스코프 체인, this 가 있다.

Eval Function Execution Context (Eval 실행 컨텍스트)

eval() 함수를 실행해서 만들어진 EC를 말한다.

EC는 어떻게 생기는거야?

Creation phase(생성 단계)Execution phase(실행 단계) 를 거치며 생성된다.

Creation phase

생성 단계는 JS 엔진이 함수를 호출했지만 실행이 시작되지 않은 단계이다.

생성 단계에서 JS 엔진은 컴파일 단계에 있으며 코드를 컴파일하기 위해 (함수를) 스캔한다.

-참고3

  1. LexicalEnvironment 컴포넌트를 만든다.
  2. VariableEnvironment 컴포넌트를 만든다.

1, 2 단계를 거친 EC는 대략적으로 아래와 같다.

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}

각각의 단계를 자세하게 알아보자.

LexicalEnvironment 컴포넌트 생성

Lexical Environment는 자바스크립트 코드에서 변수나 함수 등의 식별자를 정의하는데 사용하는 객체로 생각하면 쉽다.

Lexical Environment는 **식별자와 참조 혹은 값을 기록하는 Environment Record**와 outer라는 또 다른 Lexical Environment를 참조하는 포인터로 구성된다.

outer는 외부 Lexical Environment를 참조하는 포인터로, 중첩된 자바스크립트 코드에서 스코프 탐색을 하기 위해 사용한다.

-참고4

Environment Recordouter 를 조금 더 이해하기 쉽게 아래 코드와 구조를 살펴보자 (물론 실제로 이렇게 단순하게 동작한다는 것은 아니지만 개념적으로 쉽게 이해할 수 있다).

function foo() {
  const a = 1;
  const b = 2;
  const c = 3;
  function bar() {}

  // 2. Running execution context

  // ...
}

foo(); // 1. Call
// Running execution context의 LexicalEnvironment

{
  environmentRecord: {
    a: 1,
    b: 2,
    c: 3,
    bar: <Function>
  },
  outer: foo.[[Environment]]
}
LexicalEnvironment 컴포넌트는 어떻게 생성될까?

LexicalEnvironment는 EC와 함께 함수의 호출 단계 중 PrepareForOrdinaryCall 단계에서 만들어진다.

함수 호출의 3가지 단계
1. PrepareForOrdinaryCall // 이 단계에서!
2. OrdinaryCallBindThis
3. OrdinaryCallEvaluateBody
/* PrepareForOrdinayCall(F, newTarget) */

callerContext = runningExecutionContext;
calleeContext = new ExecutionContext;
calleeContext.Function = F;

// 바로 여기, Execution Context를 만든 직후 Lexical Environment를 생성한다.
localEnv = NewFunctionEnvironment(F, newTarget); // 호출!

// --- LexicalEnvironment와 VariableEnvironment의 차이는 서두에 있는 링크를 참고하자.
calleeContext.LexicalEnvironment = localEnv;
calleeContext.VariableEnvironment = localEnv;

executionContextStack.push(calleeContext);
return calleeContext;
/* NewFunctionEnvironment(F, newTarget) */

env = new LexicalEnvironment;
envRec = new functionEnvironmentRecord;
envRec.[[FunctionObject]] = F;

if (F.[[ThisMode]] === lexical) { // this 초기화
  envRec.[[ThisBindingStatus]] = 'lexical';
} else {
  envRec.[[ThisBindingStatus]] = 'uninitialized';
}

home = F.[[HomeObject]];
envRec.[[HomeObject]] = home; 
envRec.[[NewTarget]] = newTarget; 

env.EnvironmentRecord = envRec. // EnvironmentRecord 초기화
env.outer = F.[[Environment]]; // outer 초기화

return env;

즉, NewFunctionEnvironment()Environment Recordouter를 가진 Lexical Environment를 만들어 반환한다. 여기에 함수 환경으로 this, super, new.target등의 정보를 Environment Record에 함께 초기화했다.

this 의 바인딩은 이 시점 에서 일어나는 것이다.

1. Environment Record - 식별자 바인딩

Environment Record는 식별자들의 바인딩을 기록하는 객체를 말한다. 간단히 말해 변수, 함수 등이 기록되는 곳이다.

실질적으로 Declarative Environment Record와 Object Environment Record 두 종류로 생각할 수 있으며,

이외에 조금 더 자세히 보면 Global Environment Record, Function Environment Record, Module Environment Record가 있다.

이들은 다음과 같은 상속 관계를 갖는다.

-참고4

                                           Environment Record
                                                    |
                    -----------------------------------------------------------------
                    |                               |                               |
        Declarative Environment Record     Object Environment Record     Global Environment Record
                    |
            --------------------------------
            |                              |
Function Environment Record     Module Environment Record

우리는 변수와 함수에 대해서 볼 것이므로, Declarative Environment RecordObject Environment Record 를 살펴보자.

  • Declarative Environment Record
    • 변수나 함수의 선언에 대한 정보가 담겨있다.
  • Function Environment Record
    • 위(NewFunctionEnvironment)에서 언급한 new.target, this, super 등에 대한 정보가 담겨있다.
2. Reference to the outer environment - 스코프 체인

outer 는 스코프 체인 내에서 식별자를 찾을 수 있도록 하는, 스코프 체인 에 대한 참조 이다. 즉 outer 를 통해서 (스코프 체인을 따라가면서) 식별자를 찾는다.

3. this binding

this 의 바인딩은 위에서 언급한 NewFunctionEnvironment() 에서 일어나지만, 보다 자세하게 여기서 추가로 적어보겠다.

  • 전역 EC일 때

    • this 는 전역 객체를 나타낸다. (브라우저의 경우 Window Object 를 나타낸다)
  • 함수 EC일 때

    • this 는 동적으로 바인딩 된다. (함수를 누가 호출하느냐에 따라서 this가 달라진다)
    • 왜냐하면, 엔진이 코드를 실행할 때 (전역, eval 그리고) 함수 단위로 EC를 만들어서 Call Stack에 추가를 한다. 이 때 EC는 (LexicalEnvorionment와 더불어) NewFunctionEnvironment() 라는 함수로 인해 생성이 되는데, 이 때 this 의 바인딩이 일어난다. 이로 인해 함수의 this 는 동적으로 바인딩이 되는 것이다.
const person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}
person.calcAge(); 
// 'calcAge'가 'person' 객체를 참조하여 호출되었으므로 'this'는 'person'을 나타낸다.

const calculateAge = person.calcAge;
calculateAge();
// 'this'는 객체 참조가 없기 때문에 전역 (브라우저에서는 window) 객체를 나타낸다.
/* 위 코드의 Lexical Environment를 슈도코드로 표현한다면 이렇다 */
GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
    }
    outer: <null>,
    this: <global object>
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}

VariableEnvironment 컴포넌트 생성

LexicalEnvironment 와 마찬가지로 EC내에서 식별자의 바인딩에 대한 정보를 갖고 있는다.

LexicalEnvironment 와의 차이점은, LexicalEnvironmentlet, const에 대한 바인딩을 저장하지만 VariableEnvironmentvar에 대한 바인딩만 저장한다.

Note

생성 단계(Creation phase)에서 EC가 생성되면, 엔진은 해당 컨텍스트에서 실행에 필요한 여러 가지 정보를 담을 객체를 생성한다. 그리고 이를 활성 객체(변수 객체)라고 한다. 즉 EC 안에 활성 객체가 있고 그 안에 아래의 값들이 있는 것이다.

  1. 스코프 체인
  2. arguments 객체 생성
  3. 변수의 생성 (할당 말고)
  4. this 의 바인딩

그리고 이것들은 컴파일 과정을 거치며 만들어진다. 즉 생성 단계는 컴파일레이션 과정과 같고, EC는 컴파일레이션의 단위이다. 컴파일레이션 참고

활성 객체를 코드와 그림으로 알아보자.

function execute(param1, param2) {
  var a = 1, b = 2;
  function func() {
    return a+b;
  }
  return param1 + param2 + func();
}

execute(3, 4); // 실행 시

execute(3, 4) 를 실행했을 때, EC가 생성 단계를 거치고 난 후의 모습이다.

ec

Execution phase

앞서 선언한 변수에 모두 할당을 하고, 코드를 실행하는 단계이다.

코드와 그림으로 보다 직관적으로 알아보자.

let a = 20;
const b = 30;
var c;
function multiply(e, f) {
 var g = 20;
 return e * f * g;
}
c = multiply(20, 30);

1

이 때 a(let), b(const)와 c(var)의 할당된 값이 다른 이유는 letconst는 값이 할당이 되기 전 까지 접근할 수 없도록 하기 위함이다. (letconst 는 할당되기 전에 사용할 경우 ReferenceError 가 나는 이유와도 같다)

2

3 4 5

6

Note

1번 그림에서 짧게 이야기했지만 var 로 선언된 c는 생성 단계에서 undefined 를 갖는다. 그리고 이는 아래 코드와 같이, var 로 선언된 변수가 호이스팅이 일어나는 이유와도 같다.

즉 실행 전에, 미리 선언(만)을 다 시켜버리기 때문에, 선언이 어떤 변수를 사용하는 부분 보다 아래에 되어 있더라도 문제없이 사용이 가능한 것이다.

a = 10;
console.log(a); // 10
var a;
/* 위 코드는 컴파일 과정(Creation Phase)을 거치면, 아래와 같은 코드로 바뀐다 */
var a;
a = 10;
console.log(a);

추가적으로 const, let은 호이스팅이 일어나지 않는 것이 아니다. (사실 일어난다)

const, let 은 값이 할당이 되기 전 까지(Execution phase에서 할당 코드가 실행되기 전 까지) TDZ(Temporal Dead Zone)라는 곳에 머무른다. 어떤 변수가 이 곳에 있는 동안은 스코프 내에 있는 것이라 생각하지 않기 때문에 엔진이 해당 변수를 찾지 못한다.

그리고 할당이 되면, 이 곳에서 벗어나 참조(RHS 탐색)를 할 수 있는 상태가 되는 것이다. 즉 할당이 되기 전 까지는 변수를 찾을 수가 없는 것이다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment