실행할 코드에 제공할 환경 정보들을 모아놓은 객체
하나의 실행 컨텍스트를 구성할 수 있는 방법으로 전역공간, eval()함수, 함수 등이 있습니다.
자동으로 생성되는 전역공간과 악마로 취급받는 eval을 제외하면 우리가 흔히 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것뿐입니다.
※ ES6에서는 블록 {}에 의해서도 새로운 실행 컨텍스트가 생성
// ------------------------------ (1)
var a = 1;
function outer() {
function inner() {
console.log(a);
var a = 3;
}
inner(); // ----------------- (2)
console.log(a);
}
outer(); // --------------------- (3)
console.log(a);
위 코드를 보며 실행 컨텍스트가 콜 스택에 어떤 순서로 쌓이고 실행되는 지 살펴보겠습니다.
처음 자바스크립트 코드를 실행하는 순간(1) 전역 컨텍스트가 콜 스택에 담깁니다. (3)에서 outer 함수를 호출하면 자바스크립트 엔진은 outer에 대한 환경 정보를 수집해서 outer 실행 컨텍스트를 생성한 후 콜 스택에 담습니다. 전역 컨텍스트와 관련된 코드의 실행을 중단하고 outer 실행 컨텍스트와 관련된 코드를 순차적으로 실행합니다. 다시 (2)에서 inner 함수의 실행 컨텍스트가 콜 스택의 가장 위에 담기면 outer 컨텍스트는 중단하고 inner 함수 내부 코드를 진행합니다. 실행이 종료되면 콜 스택에서 제거되고 그 다음 순서에 있는 코드를 이어서 진행합니다.
어떤 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드를 실행하는 데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장합니다. 여기에 담기는 정보들은 다음과 같습니다.
LexicalEnvironment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 다릅니다. 실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LexicalEnvironment를 만들고 이후에는 LexicalEnvironment를 주로 활용합니다. 이들 내부는 environmentRecord와 outerEnvironmentReference로 구성돼 있습니다. 자세한 내용은 LexicalEnvironment와 같으므로 아래에서 살펴보겠습니다.
environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됩니다. 컨텍스트 내부 전체를 처음부터 끝가지 쭉 훑어나가며 순서대로 수집(함수에 지정된 매개변수명, 선언한 함수가 있을 경우 그 함수 자체, 변수명)합니다.
수집이 끝나더라도 아직 실행 컨텍스트가 관여할 코드들은 실행되기 전의 상태입니다. 코드가 실행되기 전임에도 불구하고 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있게 되는 셈이죠. 여기서 호이스팅(hoisting)이라는 개념이 등장합니다. 호이스팅이란 '끌어올리다'라는 의미이고 코드 정보를 수집하는 과정을 이해하기 쉬운 방법으로 대체한 가상의 개념입니다. 자바스크립트 엔진이 실제로 끌어올리지는 않지만 편의상 끌어올린 것으로 간주하자는 것이죠.
environmentRecord에는 매개변수명, 선언된 함수, 변수명 등이 담긴다고 했습니다. 몇가지 예제를 통해 살펴보겠습니다. 아래는 호이스팅 전의 모습입니다.
function a () {
var x = 1;
console.log(x);
var x;
console.log(x);
var x = 2;
console.log(x);
}
a();
다음은 호이스팅을 마친 상태입니다. environmentRecord는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 어떤 값이 할당될 것인지는 관심이 없습니다. 따라서 변수를 호이스팅할 때 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 남겨둡니다.
function a () {
var x;
var x;
var x;
x = 1;
console.log(x); // 1
console.log(x); // 1
x = 2;
console.log(x); // 2
}
다음은 함수가 포함된 코드의 호이스팅입니다.
function a () {
console.log(b);
var b = 'bbb';
console.log(b);
function b () {}
console.log(b);
}
a();
a 함수를 실행하는 순간 a 함수의 실행 컨텍스트가 생성되고 이때 변수명과 함수 선언의 정보를 위로 끌어올립니다(수집합니다). 변수는 선언부와 할당부를 나누어 선언부만 끌어올리는 반면 함수 선언은 함수 전체를 끌어올립니다.
function a () {
var b;
function b () {}
console.log(b); // b 함수
b = 'bbb';
console.log(b); // bbb
console.log(b); // bbb
}
a();
호이스팅을 다루는 김에 함께 알아두면 좋은 내용입니다.
함수 선언문은 function 정의부만 존재하고 별도의 할당 명령이 없는 것을 의미하고, 함수 표현식은 정의한 function을 별도의 변수에 할당하는 것을 말합니다. 함수 선언문의 경우 반드시 함수명의 정의돼 있어야 하는 반면, 함수 표현식은 없어도 됩니다.
function a () {} // 함수 선언문. 함수명 a가 곧 변수명.
a(); // 실행 OK
var b = function () {} // (익명)함수 표현식. 변수명 b가 곧 함수명.
b(); // 실행 OK
var c = function () {} // 기명 함수 표현식. 변수명은 c, 함수명은 d.
c(); // 실행 OK
d(); // 에러!
아래 예제를 통해 함수 선언문과 함수 표현식의 실질적인 차이를 살펴보겠습니다.
console.log(sum(1, 2));
console.log(multiply(3, 4));
function sum (a, b) {
return a + b;
}
var multiply = function (a, b) {
return a * b;
}
아래는 호이스팅을 마친 상태입니다.
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;
}
함수 선언문은 전체를 호이스팅한 반면 함수 표현식은 선언부만 호이스팅 했습니다. 따라서 첫 번째 sum 함수의 호출은 잘 실행되는 반면 mutiply 함수의 호출은 ‘multiply is not a function’ 이라는 에러 메시지가 출력됩니다. 함수 선언문의 경우 함수를 선언하기도 전에 문제 없이 실행되는 것이 받아들이기 어려울 수도 있습니다. 함수 선언문 보다는 선언한 후에 호출할 수 있는 함수 표현식이 상대적으로 안전합니다.
스코프란 식별자에 대한 유효범위입니다. 어떤 경계 A의 외부에서 선언한 변수는 A의 외부 뿐 아니라 A의 내부에서도 접근이 가능하지만, A의 내부에서 선언한 변수는 오직 A의 내부에서만 접근할 수 있습니다. 이러한 스코프의 개념은 대부분의 언어에 존재합니다.
ES5까지의 자바스크립트는 전역공간을 제외하면 오직 함수에 의해서만 스코프가 생성됩니다(함수 스코프). ES6부터는 블록에 의해서도 스코프 경계가 발생하는데(블록 스코프), var에는 적용되지 않고 오직 새로생긴 let과 const 등에서만 적용됩니다.
이러한 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인이라고 합니다. 그리고 이것을 가능하게 하는 것이 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference 입니다.
outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조합니다. 예를들어 A 함수 내부에 B 함수를 선언하고 다시 B 함수 내부에 C 함수를 선언한 경우, 함수 C의 outerEnvironmentReference는 함수 B의 LexicalEnvironment를 참조합니다. 함수 B의 outerEnvironmentReference는 다시 함수 B가 선언되던 때(A)의 LexicalEnvironment를 참조하겠죠.
이처럼 outerEnvironmentReference는 연결리스트 형태를 띱니다. '선언 시점의 LexicalEnvironment'를 계속 찾아 올라가면 마지막엔 전역 컨텍스트의 LexicalEnvironment가 있겠죠. 또한 각 outerEnvironmentReference는 오직 자신이 선언된 시점의 LexicalEnvironment만 참조하고 있으므로 가장 가까운 요소부터 차례대로만 접근할 수 있고 다른 순서로 접근하는 것은 불가능합니다. 따라서 여러 스코프에서 동일한 식별자를 선언한 경우 무조건 스코프 체인상에서 가장 먼저 발견된 식별자에만 접근 가능하게 됩니다.
아래 코드의 흐름에 따라 좀 더 구체적으로 알아보겠습니다.
var a = 1;
var outer = function () {
var inner = function () {
console.log(a); // ---------- (1)
var a = 3;
}
inner();
console.log(a); // -------------- (2)
}
outer();
console.log(a); // ------------------ (3)
전역 컨텍스트가 활성화됩니다. 전역 컨텍스트의 environmentRecord에는 a 와 outer 식별자가 저장됩니다. 변수 a에 1을, 변수 outer에 함수를 할당합니다.
outer 함수가 호출되고 outer 실행 컨텍스트가 활성화됩니다. outer environmentRecord에는 inner 식별자가 저장됩니다. 변수 inner에 함수를 할당합니다. outer 함수는 전역 공간에서 선언되었으므로 outerEnvironmentReference에는 전역 컨텍스트의 LexicalEnvironment가 담깁니다. [ GLOBAL, {a, outer}]
inner 실행 컨텍스트가 활성화됩니다. inner environmentRecord에는 a 식별자가 저장됩니다. inner 함수는 outer 함수 내부에서 선언되었으므로 outerEnvironmentReference에는 outer 함수의 LexicalEnvironment가 담깁니다. [ outer, { inner }]
(1)에서 식별자 a에 접근하고자 합니다. 현재 활성화 상태인 inner 컨텍스트의 environmentRecord에서 a를 검색합니다. a가 발견됐는데 아직 할당된 값이 없습니다(undefined 출력). a에 3을 할당하고 inner 함수 실행이 종료되며 실행 컨텍스트가 콜 스택에서 제거됩니다.
(2)에서 식별자 a에 접근하고자 합니다. 현재 활성화 상태인 outer 컨텍스트의 environmentRecord에서 a를 검색합니다. a가 존재하지 않으므로 outerEnvironmentReference에 있는 environmentRecord(전역 컨텍스트)로 넘어가는 식으로 계속해서 검색합니다. a가 있고 그 a에 저장된 값 1을 출력합니다(1 출력). outer 함수 실행이 종료되고 outer 실행 컨텍스트가 콜 스택에서 제거됩니다.
(3)에서 식별자 a에 접근하고자 합니다. 현재 활성화 상태인 전역 컨텍스트의 environmentRecord에서 a를 검색합니다. 바로 a를 찾을 수 있습니다(1 출력). 전역 컨텍스트가 콜 스택에서 제거되고 종료됩니다.
위 코드상의 식별자 a는 전역 공간에서도 선언했고 inner 함수 내부에서도 선언했습니다. inner 함수 내부에서 a에 접근하려고 하면 무조건 스코프 체인 상의 첫번째 인자, 즉 inner 스코프의 LexicalEnvironment부터 검색할 수밖에 없습니다. inner 스코프의 LexicalEnvironment에 a 식별자가 존재하므로 스코프 체인 검색을 더 진행하지 않고 즉시 inner LexicalEnvironment 상의 a를 반환하게 됩니다. 즉 전역 공간에서 선언한 동일한 이름의 a 변수에는 접근할 수 없는 셈입니다. 이를 변수 은닉화라고 합니다.
출처
코어 자바스크립트
자바스크립트 실행 컨텍스트