
최근에 Prototype에 관해 정리했는데, this 이야기가 빠지지 않고 나왔습니다. 그래서 this를 공부하려고 하니, this는 실행 컨텍스트로부터 파생되는 개념이었죠.
끊임없는 공부 체이닝으로 '실행 컨텍스트'라는 주제까지 도달했습니다. 깔끔하게 정리해 보도록 하겠습니다. 오늘은 '코어 자바스크립트'라는, 아주 대단한 책을 참고했습니다.
실행 컨텍스트(Execution Context)는 실행할 코드에 제공할 '환경 정보'들을 모아놓은 '객체'입니다.
// -------------------------- (1)
var a = 1;
function outer() {
function inner() {
console.log(a); // undefined
var a = 3;
}
inner(); // ------------ (2)
console.log(a); // 1
}
outer(); // ---------------- (3)
console.log(a); // 1
위 코드를 실행하는 순간, 전역 컨텍스트라는 객체가 콜 스택이라는 우물에 들어갑니다. 최상단 공간은 코드 내부에서 별도의 실행 명령이 없더라도 브라우저에서 자동으로 실행하므로, 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성화됩니다.
(3)에서 outer() 함수를 호출하면, 자바스크립트 엔진은 outer에 대한 환경 정보를 수집해서 outer 실행 컨텍스트를 생성한 후 콜 스택에 담습니다.
콜 스택의 가장 위에 있는 outer() 함수를 먼저 처리하는데, 내부에서는 inner() 함수가 호출됩니다. 따라서 (2)에서 inner() 함수의 실행 컨텍스트가 콜 스택의 가장 위에 추가됩니다. 이제는 inner() 함수가 콜 스택의 최상단에 위치하니, inner()를 먼저 처리합니다.
inner()에서 변수 a에 3을 할당하면 inner 함수의 실행이 종료되며 inner 실행 컨텍스트가 콜 스택에서 제거됩니다. a의 값을 출력하면 outer 실행 컨텍스트가 콜 스택에서 제거되겠죠. 전역 공간에서 실행할 코드가 더 남아 있지 않기에 전역 컨텍스트가 제거되고, 콜 스택에는 아무것도 남지 않게 됩니다. 이 과정을 도식화하면 아래의 이미지와 같습니다.

활성화된 실행 컨텍스트는 아래 이미지와 같은 정보를 수집하게 됩니다. 하나하나 살펴볼 예정입니다.

VariableEnvironment에 담기는 내용은 LexicalEnvironment와 동일하지만 최초 실행 시의 스냅샷을 유지한다는 점에서 다릅니다.
실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용하게 됩니다.
현재 단계에서는, VariableEnvironment는 실행 컨텍스트를 생성할 때, 정보를 담기 위해 처음으로 사용하는 그릇이라고만 이해해도 무방합니다.
environmentRecord는 보이지 않습니다. 그래서 어렵게 느껴지죠. 호이스팅과 함수 선언문 / 함수 표현식이라는 키워드로 내부 동작 원리를 설명하고자 합니다.
environmentRecord에는 매개변수의 이름, 함수, 변수명 등이 담깁니다. 컨텍스트 내부를 처음부터 끝까지 훑어나가며 순서대로 해당 요소들을 수집하게 됩니다. 코드가 실행되기도 전에 자바스크립트 엔진은 해당 환경에 속한 코드의 변수명들을 모두 알고 있게 되는 셈입니다.
그래서 개발자가 겉으로 봤을 때에는, 마치 자바스크립트 엔진이 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행하는 것처럼 보입니다.
이러한 상황을 호이스팅(Hoisting)이라는 가상의 개념으로 설명하는 것입니다. 실제로 자바스크립트 엔진은 식별자들을 끌어올린 적이 없습니다.
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)에서는 전달한 1을 받아서 출력하고, (2)는 선언된 변수에 할당한 값이 없기에 undefined가 출력되고, 마지막 (3)에서는 2가 출력될 것 같습니다.
위 논리를 코드로 표현하면 다음과 같습니다.
function a() {
var x = 1; // 수집 대상 1(매개변수 선언)
console.log(x); // (1)
var x; // 수집 대상 2(변수 선언)
console.log(x); // (2)
var x = 2; // 수집 대상 3(변수 선언)
console.log(x); // (3)
}
a();
이제 호이스팅이 적용된 버전의 코드를 살펴보시죠.
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);
앞서 설명한 대로, 변수를 순서대로 수집했습니다. environmentRecord는 식별자에만 관심이 있으니, 할당 과정은 그대로 남겨두었습니다.
실제 출력 결과는 1, undefined, 2가 아니라 1, 1, 2가 됩니다. 수집 대상 1의 변수 x가 있으니 후속 변수 선언 두 줄은 무시되죠.
함수는 세 가지 방식으로 정의할 수 있습니다. 함수 선언문 / 함수 표현식(익명) / 함수 표현식(기명)이 바로 그것이죠. 사실 화살표 함수도 있는데, 화살표 함수에 대해서는 this에서 다룰 예정입니다.
보통은, 함수 표현 방식에 세 가지가 있다고 할 때, 함수 선언문 / 함수 표현식 / 화살표 함수가 있다고 하는 것이 맞습니다.
function a() {
/* ... */
} // 함수 선언문. 함수명 a가 곧 변수명.
a(); // 실행 OK.
var b = function() {
/* ... */
}; // (익명) 함수 표현식. 변수명 b가 곧 함수명.
b(); // 실행 OK.
var c = function d() {
/* ... */
}; // 기명 함수 표현식. 변수명은 c, 함수명은 d.
c(); // 실행 OK.
d(); // 에러!
함수 선언문과 함수 표현식의 예제 코드를 살펴보시죠.
console.log(sum(1, 2));
console.log(multiply(3, 4));
function sum(a, b) {
// 함수 선언문 sum
return a + b;
}
var multiply = function(a, b) {
// 함수 표현식 multiply
return a * b;
};
위 코드가 호이스팅을 마친 상태를 아래와 같습니다.
var sum = function sum(a, b) {
// 함수 선언문은 전체를 호이스팅합니다.
return a + b;
};
var multiply; // 변수는 선언부만 끌어올립니다.
console.log(sum(1, 2));
console.log(multiply(3, 4));
multiply = function(a, b) {
// 변수의 할당부는 원래 자리에 남겨둡니다.
return a * b;
};
정상적으로 3이 출력되고, 그다음에는 multiply is not a function이 출력될 것입니다. 함수 선언문은 전체가 호이스팅 되었지만, 함수 표현식은 변수 선언부만 호이스팅 되기에 multiply에 아무런 값도 할당되지 않은 상태이기 때문이죠.
그런데 갑자기 이런 생각이 들었습니다. environmentRecord를 다루는데 왜 호이스팅과 함수 선언에 대한 이야기를 하지? environmentRecord가 식별자를 수집하는 과정을 설명하고 싶었던 건가?
결론은 var와 함수 선언문을 사용하지 말자는 것입니다.
var를 사용했을 때, 선언 전에 접근해도 에러 대신 undefined를 반환하거나, 함수 선언문이 코드의 어느 위치에 있든 호출이 가능해지는 현상은 코드의 실행 순서를 예측하기 어렵게 만듭니다. 이는 곧 잠재적인 버그의 원인이 되며, 코드의 가독성과 유지 보수성을 크게 떨어뜨립니다.
environmentRecord는 실행 컨텍스트에서 매개변수의 이름, 함수, 변수명 등이 담기는데, 그 원리를 var와 함수 선언을 통해 설명하며, 동시에 우리가 어떤 코드를 지양해야 하는지 학습한 것이죠.
스코프는 식별자의 유효범위입니다. A 함수 내부에서 선언한 변수는 오직 A의 내부에서만 접근할 수 있다는 것이 스코프의 개념이죠.
식별자의 유효범위인 스코프를, 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인(scope chain)이라고 합니다.
이러한 체이닝을 가능케 하는 것이 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference입니다.
outerEnvironmentReference는, 현재 호출된 함수가 '선언될 당시의' LexicalEnvironment'만' 참조합니다.
코드를 바로 살펴보시죠.
// 1
var a = 1;
// 2
var outer = function() {
// 3
var inner = function() {
// 4
console.log(a);
// 5
var a = 3;
// 6
};
// 7
inner();
// 8
console.log(a);
// 9
};
// 10
outer();
// 11
console.log(a);
이제 위 코드의 스코프 체인을 이미지로 표현해 보겠습니다.
이미지에 표현된 숫자는 코드 상에 주석으로 표기한 숫자와 일치합니다.
추가적으로 L.E는 LexicalEnvironment를, e는 environmentRecord를, o는 outerEnvironmentReference를 의미합니다.

실행 컨텍스트에는 VariableEnvironment와 LexicalEnvironment 말고도 thisBinding이라는 것이 남아있습니다.
실행 컨텍스트 활성화 당시에 this가 지정되지 않은 경우 this에는 전역 객체가 저장됩니다. 그밖에 함수를 호출하는 방법에 따라 this에 저장되는 대상이 다릅니다.
this에 관한 글을 이미 작성했지만, 내일 조금 더 수정과 보완을 거친 뒤 게시하고자 합니다.