실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체. 동일한 환경에 있는 코드들을 실행할 때, 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 call stack
에 쌓아 올린다. 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.
하나의 실행 컨텍스트를 구성할 수 있는 방법으로 전역공간
, eval()
, 함수
등이 있다.
콜 스택에서 새로운 스택이 쌓일 때, 자바스크립트 엔진은 새로운 스택에 대한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장한다.
실행 컨텍스트에 담기는 정보는 아래와 같다.
VariableEnvironment
: 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보, 선언 시점의 LexicalEnvironment의 스냅샷.
LexicalEnvironment
: 처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨.
ThisBinding
: this 식별자가 바라봐야할 대상 객체
VariableEnvironment
에 담기는 내용은 LexicalEnvironment
와 같지만, 최초 실행 시의 스냅샷을 유지한다는 차이가 있다. 실행 컨텍스트를 생성할 때 VariableEnvironemnt
에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LecicalEnvironment
를 만들고, 이 후에는 LexicalEnvironment
를 활용한다.
VariableEnvironment
와 LexicalEnvironment
의 내부는 environmentRecord
와 outerEnvironmentReference
로 구성되어 있다.
environmentRecord
에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트 구성 함수에 지정된 매개볂수 식별자, 선언한 함수, var로 선언한 변수의 식별자 등을 저장하며, 자바스크립트 엔진이 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 그 정보를 순서대로 수집한다.
💡 전역 실행 컨텍스트는 자바스크립트 구동환경이 별도로 제공하는 객체를 사용한다.
브라우저의 경우 window, Node.js의 경우 global 객체 등이 이에 해당한다.
이들은 자바스크립트 내장 객체가 아닌 호스트 객체로 분류된다.
변수 정보를 수집하는 과정을 모두 마쳐도, 실제 코드의 실행은 되지 않은 상태이다. 자바스크립트 엔진은 코드가 실행되지 않았지만, 해당 환경에 속한 식별자들을 모두 알고 있다. 여기서 호이스팅(hoisting)의 개념이 등장한다.
'코드가 실행되지 않았지만 자바스크립트 엔진은 해당 환경의 식별자들을 모두 알고 있다.' 이 말을 우리가 알고 있는 '자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음, 실제 코드를 실행한다.' 라는 설명으로 바꾼 것이다. 이렇게 서술해도 문제될 것이 전혀 없고, 개념을 받아들이기 더 쉬워서 이런 가상의 개념을 만든 것으로 보인다.
environmentRecord
에는 매개변수의 이름, 함수 선언, 변수명이 담긴다. 할당, 초기화에는 관심이 없으며 그대로 남겨두고, 오직 변수명만 가져 온다. 이를 코드로 가시화를 하자면 아래와 같이 된다고 볼 수 있다.
// 우리가 작성하는 코드
function a() {
var x = 1;
console.log(x);
var x;
console.log(x);
var x = 2;
console.log(x);
}
a();
// Hoisting의 가시화
function a() {
var x;
var x;
var x;
x = 1;
console.log(x); // 1
console.log(x); // 1
x = 2;
console.log(x); // 2
}
a();
호이스팅은 실행 컨텍스트가 구성될 때 일어난다. 다시 말해 함수가 실행 될 때 일어난다.
그렇다면 변수 선언이 아닌 함수 선언의 호이스팅은 어떻게 작동할까?
// 호이스팅 전
function a () {
console.log(b);
var b = 'bbb';
console.log(b);
function b () {}
console.log(b)
}
a();
// 호이스팅 후
function a() {
var b;
function b() {};
console.log(b); // [Function: b]
b = 'bbb';
console.log(b); // 'bbb'
console.log(b); // 'bbb'
}
a();
변수는 선언부와 할당 부를 나누어 선언부만 끌어올리는 반면, 함수 선언은 함수 전체를 끌어올린다.
예전 함수 선언문과 함수 표현식에 대해 작성했던 포스팅
함수 선언문은 function 정의부만 존재하고, 할당 명령이 없다. 반면 함수 표현식은 function의 정의를 별도의 변수에 할당한다.
함수의 이름을 작성하느냐, 작성하지 않느냐에 따라 익명 함수, 기명 함수로 나눌 수 있는데 함수 선언문의 경우 반드시 함수의 이름이 표기되어야 한다. 반면 함수 표현식은 함수 이름을 작성하지 않고 함수를 정의할 수 있다.
// 함수 선언문
function a () {
console.log('a');
}
// 함수 표현식 - 익명 함수 표현식
var b = function () {
console.log('b');
}
// 함수 표현식 - 기명 함수 표현식
var c = function d () {
console.log('c')
}
a(); // a
b(); // b
c(); // c
d(); // Reference Error
기명 함수 표현식의 경우, 함수 외부에서 함수 이름으로 호출이 불가능하다. 하지만 내부에서는 함수 호출이 가능한데, 재귀 함수에서 이를 활용할 수 있다.
let i = 0;
var c = function d () {
console.log('c')
i++;
if(i === 10) return;
d();
}
c();
호이스팅이 일어 날 때, 변수는 선언부만을, 함수는 함수의 선언 전체를 호이스팅 시킨다고 했다. 함수 표현식은 함수의 선언을 변수에 저장하므로 변수의 호이스팅처럼 호이스팅이 일어난다. 반면 함수 선언문의 경우, 함수 선언 전체가 호이스팅된다. 따라서 아래 코드의 실행 결과를 예측할 수 있다.
console.log(sum(1,2)); // 3
console.log(multi(2,8)) // TypeError
// 함수 선언문 - 함수 선언 전체를 호이스팅
function sum (a, b) {
return a + b;
}
// 함수 표현식 - 변수 호이스팅
var multi = function(a, b) {
return a * b;
}
2번째 줄 multi()의 경우, undefined가 아닌 TypeError를 반환하는 이유는 실행 컨텍스트 구성 시 multi를 변수로 다루기 때문에 호출자를 붙일 경우, 함수가 아니라는 에러를 반환하게 된다. 해당 줄을 console.log(multi)
로 바꾸게 될 경우, undefined
가 출력되는 것을 확인할 수 있다.
스코프란 식별자에 대한 유효 범위이다. 식별자의 유효 범위를 안에서부터 바깥으로 차례로 검색해 나가는 것을 스코프 체인이라 한다. 이때 바깥이긴 한데, 어느 쪽 바깥으로 검색을 해 나갈지 그 참조를 저장하는 곳이 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference
이다.
예를 들어 A 함수 내부에 B 함수를 선언하고, B 함수 내부에 C 함수를 선언한 경우를 생각해보자. C 함수의 outerEnvironmentReference
는 B 함수의 LexicalEnvironment를 참조하고 B 함수의 outerEnvironmentReference
는 A 함수의 LexicalEnvironment를 참조하게 된다. C -> B -> A 와 같은 형태를 띄는 outerEnvironmentReference
는 연결 리스트(linked-list) 형태를 갖게 된다.
var a = 1;
var outer = function () {
var inner = function () {
console.log(a);
var a = 3;
}
inner();
console.log(a);
}
outer();
console.log(a);
위 코드의 실행 결과를 확인하면서, 실행 컨텍스트와 렉시컬 환경, 스코프 체인에 대해 좀 더 확실하게 이해할 수 있었다.
var a
, var outer
를 호이스팅한 뒤 undefined로 초기화 한다. outer는 함수 표현식이므로 변수만 호이스팅이 일어나고 실제 함수 선언의 할당은 해당 코드에 도달해야 일어난다.var a = ...
, var outer = function() {...}
코드를 실행하며 변수에 값을 할당한다.LexicalEnviroment
중 outerEnvironmentReference
는 global을 가리킨다. EnvironmentRecord
는,var inner
를 호이스팅한다. 그 뒤 코드를 실행한다. inner에 익명 함수가 할당되고, inner()를 실행한다.outerEnvironmentReference
는 outer를 가리킨다. EnvironmentRecord
는 var a
를 호이스팅한다. console.log(a)
를 실행한다. 나는 여기서 당연히 1이 출력될 거라 생각했는데, undefined
가 출력된다. 여기서 각각의 함수는 고유한 렉시컬 환경을 갖는다는 것을 깨달았다. inner의 LexicalEnviroment
에서는 var a = 3
이라는 코드가 존재하기 때문에, 이를 호이스팅한 뒤 undefined
로 초기화한다.LexicalEnviroment
에는 변수 a가 존재하지 않는다. 이 때 스코프 체인이 일어난다. 자바스크립트 엔진은 함수 b의 outerEnvironmentReference
를 참조하여 전역 실행 컨텍스트에서 변수 a를 찾는다. 전역 실행 컨텍스트에는 변수 a의 호이스팅, 값 할당까지 완료가 되었으므로, 변수 a에서 1이라는 값을 읽은 뒤 이를 console.log
로 출력한다. 따라서 이 때는 1이 출력된다.console.log(a)
를 실행한다. 1을 출력한다.왜 이렇게 어렵게 만들어놨을까.