지난 JavaScript, Front-End 발표 주제는 this 였지만, 공부하다 보니 실행 컨텍스트에 대한 내용이 선행되야 할 것 같아 실행 컨텍스트에 대하여 발표를 하게 되었다.
여러 자료와 책을 참고하며 공부를 하고 있음에도 내용이 잘 와닿지 않아 참고1을 참고2, 참고3, 참고4, 책 인사이드 자바스크립트, Poiema Web을 참고하여 번역을 해보고자 한다. 추가적으로 내가 여러 자료를 찾아보면서 알게 된 내용들도 덧붙일 것이다.
실행 컨텍스트, Execution Context(이하 EC) 라는 개념은 나에겐 낯설었기에 나와 같은 사람들이 있을 것 같아 흔히들 아는 콜스택 을 시작으로 글을 써보도록 하겠다.
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는 독립적이다.
EC는 실행 가능한 자바스크립트 코드 블록이 실행되는 환경 을 말한다.
-인사이드 자바스크립트 140p
실행 컨텍스트는 Javascript 코드가 evaluate 되고, execute 되는 환경에 대한 추상적인 개념입니다.
-참고1
즉 어떤 환경(에 대한 추상적인 개념) 이라는 것인데, 이는 코드가 잘 작동할 수 있도록 변수와 함수(모든 객체)가 가진 값을 알고 이에 대한 환경을 구성하는 것을 말한다. (아래에 보다 구체적으로 다룰 것이다)
- Global Execution Context (전역 실행 컨텍스트)
- Functional Execution Context (함수 실행 컨텍스트)
- Eval Function Execution Context (eval 실행 컨텍스트)
가장 먼저 콜스택에 올라가는 EC이다. 전역 EC는 일반적인 다른 EC들과 달리 arguments
객체가 없으며, 전역 객체 하나만을 포함하는 스코프 체인과 this
가 있다.
그리고 전역 EC는 <script />
태그를 마주치면, 생성된다.
함수가 호출될 때마다 생성되는 EC로 arguments
와 스코프 체인, this
가 있다.
eval()
함수를 실행해서 만들어진 EC를 말한다.
Creation phase(생성 단계) 와 Execution phase(실행 단계) 를 거치며 생성된다.
생성 단계는 JS 엔진이 함수를 호출했지만 실행이 시작되지 않은 단계이다.
생성 단계에서 JS 엔진은 컴파일 단계에 있으며 코드를 컴파일하기 위해 (함수를) 스캔한다.
-참고3
- LexicalEnvironment 컴포넌트를 만든다.
- VariableEnvironment 컴포넌트를 만든다.
1, 2 단계를 거친 EC는 대략적으로 아래와 같다.
ExecutionContext = {
LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
VariableEnvironment = <ref. to VariableEnvironment in memory>,
}
각각의 단계를 자세하게 알아보자.
Lexical Environment는 자바스크립트 코드에서 변수나 함수 등의 식별자를 정의하는데 사용하는 객체로 생각하면 쉽다.
Lexical Environment는 **식별자와 참조 혹은 값을 기록하는
Environment Record
**와outer
라는 또 다른 Lexical Environment를 참조하는 포인터로 구성된다.
outer
는 외부 Lexical Environment를 참조하는 포인터로, 중첩된 자바스크립트 코드에서 스코프 탐색을 하기 위해 사용한다.-참고4
Environment Record
와 outer
를 조금 더 이해하기 쉽게 아래 코드와 구조를 살펴보자 (물론 실제로 이렇게 단순하게 동작한다는 것은 아니지만 개념적으로 쉽게 이해할 수 있다).
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는 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 Record
와 outer
를 가진 Lexical Environment를 만들어 반환한다. 여기에 함수 환경으로 this
, super
, new.target
등의 정보를 Environment Record에 함께 초기화했다.
즉 this
의 바인딩은 이 시점 에서 일어나는 것이다.
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 Record와 Object Environment Record 를 살펴보자.
- Declarative Environment Record
- 변수나 함수의 선언에 대한 정보가 담겨있다.
- Function Environment Record
- 위(
NewFunctionEnvironment
)에서 언급한new.target
,this
,super
등에 대한 정보가 담겨있다.
- 위(
outer
는 스코프 체인 내에서 식별자를 찾을 수 있도록 하는, 스코프 체인 에 대한 참조 이다. 즉 outer
를 통해서 (스코프 체인을 따라가면서) 식별자를 찾는다.
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>
}
}
LexicalEnvironment
와 마찬가지로 EC내에서 식별자의 바인딩에 대한 정보를 갖고 있는다.
LexicalEnvironment
와의 차이점은, LexicalEnvironment
는 let
, const
에 대한 바인딩을 저장하지만 VariableEnvironment
는 var
에 대한 바인딩만 저장한다.
생성 단계(Creation phase)에서 EC가 생성되면, 엔진은 해당 컨텍스트에서 실행에 필요한 여러 가지 정보를 담을 객체를 생성한다. 그리고 이를 활성 객체(변수 객체)라고 한다. 즉 EC 안에 활성 객체가 있고 그 안에 아래의 값들이 있는 것이다.
- 스코프 체인
- arguments 객체 생성
- 변수의 생성 (할당 말고)
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가 생성 단계를 거치고 난 후의 모습이다.
앞서 선언한 변수에 모두 할당을 하고, 코드를 실행하는 단계이다.
코드와 그림으로 보다 직관적으로 알아보자.
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
이 때 a(let
), b(const
)와 c(var
)의 할당된 값이 다른 이유는 let
과 const
는 값이 할당이 되기 전 까지 접근할 수 없도록 하기 위함이다. (let
과 const
는 할당되기 전에 사용할 경우 ReferenceError
가 나는 이유와도 같다)
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 탐색)를 할 수 있는 상태가 되는 것이다. 즉 할당이 되기 전 까지는 변수를 찾을 수가 없는 것이다.