실행할 코드에 제공할 환경 정보들을 모아놓은 객체
JavaScript Engine이 활용할 목적으로 생성할 뿐 개발자가 코드를 통해 확인할 수는 없다.
전역 컨텍스트는 별도의 실행 명령이 없어도 브라우저에서 자동으로 활성화된다.
실행 과정
현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보이다. 선언 시점의 LexicalEnvironment의 스냅샷으로, 변경 사항은 반영되지 않는다.
실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용하게 된다.
처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영된다.
실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다.
실행 컨텍스트 활성화 당시에 this가 지정되지 않은 경우 this에는 전역 객체가 저장된다. 그밖에는 함수를 호출하는 방법에 따라 this에 저장되는 대상이 다르다. // 다음 장에서 자세히 다룬다.
📢 전역 실행 컨텍스트는 변수 객체를 생성하는 대신 자바스크립트 구동 환경이 별도로 제공하는 객체, 즉 전역 객체(brower: window, Node.js: global)를 활용한다.
현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트 내부 전체를 순서대로 훑어나가며 순서대로 수집한다.
- 매개변수 식별자
- 선언한 함수 자체
- var로 선언된 변수의 식별자
변수 정보를 수집하는 과정을 모두 마쳤더라도 아직 실행 컨텍스트가 관여할 코드들은 실행되기 전의 상태이다. 코드가 실행되기 전임에도 불구하고 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있는 것이다. = hoisting
호이스팅은 코드 해석을 좀 더 수월하게 하기 위해 environmentRecord의 수집 과정을 추상화한 개념으로, 실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 '끌어올린다'고 해석하는 것이다.
function a (x) {
console.log(x); // 1
var x;
console.log(x); // 1
var x = 2;
console.log(x); // 2
}
a(1)
environmentRecord는 어떤 식별자가 있는지(선언)에만 관심이 있다. 따라서 변수를 호이스팅할 때 변수명만 끌어올리고 할당 과정은 원래 자리에 남겨둔다.
위의 코드는 다음과 같은 형태로 바뀐다.
function a (x) {
var x; // 메모리에서는 저장할 공간을 확보하고, 주소값을 변수 x에 연결해둔다.
var x; //이미 선언된 변수 x가 있으므로 무시한다.
var x; //이미 선언된 변수 x가 있으므로 무시한다.
x = 1; // 1을 별도의 메모리에 담고, 이 주솟값을 변수 x에 연결한다.
console.log(x);
console.log(x);
x = 2; // 2를 별도의 메모리에 담고, 이 주솟값을 변수 x에 연결(1의 주솟값과 대치)한다.
console.log(x)
// 함수 내부의 모든 코드가 실행됐으므로 실행 컨텍스트가 콜 스택에서 제거된다.
}
다음 코드를 보자.
function a () {
console.log(b); // [Function: b]
var b = 'bbb';
console.log(b); // bbb
function b () {}
console.log(b); // bbb
}
함수 선언은 함수 전체를 끌어올린다.
위의 코드는 다음과 같은 형태로 변환된다.
function a () {
var b // 저장 공간을 확보하고, 확보한 공간의 주소값을 변수 x에 연결해둔다.
var b = function b () {}
// 이미 선언된 변수가 있으므로 선언 과정은 무시한다.
// 함수는 별도의 메모리에 담기고 이 함수가 저장된 주솟값을 변수 b와 연결한다.
console.log(b);
b = 'bbb'; // 변수 b에 문자열 'bbb'가 담긴 주솟값으로 덮어쓴다.
console.log(b);
console.log(b);
// 함수 내부의 모든 코드가 실행됐으므로 실행 컨텍스트가 콜 스택에서 제거된다.
}
함수 선언문과 함수 표현식의 실질적인 차이를 보자.
console.log(sum(1, 2)); // 3
console.log(multiply(3, 4)); // TypeError: multiply is not a function
function sum (a, b) {
return a + b;
}
var multiply = function (a, b) {
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;
}
상대적으로 함수 표현식이 안전하다.
scope란 식별자에 대한 유효범위이다.
어떤 경계 A의 외부에서 선언한 변수는 A의 외부뿐 아니라 A의 내부에서도 접근이 가능하지만, A의 내부에서 선언한 변수는 오직 A의 내부에서만 접근할 수 있다.
'식별자의 유효범위'를 안에서부터 바깥으로 차례로 검색해나가는 것
scope chain을 가능케 하는 것이 LexicalEnvirionment의 outerEnvironmentReference이다.
outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다.
'선언하다'라는 행위가 실제로 일어날 수 있는 시점이란 콜 스택 상에서 어떤 실행 컨텍스트가 활성화된 상태일 때 뿐이다.
outerEnviornmentReference는 연결리스트 형태를 띄게 된다.
'선언 시점의 LexicalEnvironment'를 계속 찾아 올라가면 마지막엔 전역 컨텍스트의 LexicalEnvironment가 있을 것이다.
가장 가까운 요소부터 차례대로만 접근할 수 있다.
여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하게 된다.
이런 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하게 된다.
다음 코드의 실행 과정을 살펴보자.
var a = 1;
var outer = function () {
var inner = function () {
console.log(a);
var a = 3;
};
inner();
console.log(a);
};
outer();
console.log(a);
- environmentRecord에 { a, outer } 식별자 저장.
- 전역 컨텍스트는 선언 시점이 없으므로 outerEnvironmentReference에는 아무것도 담기지 않는다. (this: 전역 객체)
- 전역 스코프에 있는 변수 a에 1을, outer에 함수를 할당한다.
- environmentRecord에 { inner } 식별자 저장.
- outerEnvironmentReference에는 전역 컨텍스트의 LexicalEnvironment를 참조복사한 것이 담긴다. (this: 전역 객체)
- outer 스코프에 있는 변수 inner에 함수를 할당한다.
- environmentRecord에 { a } 식별자 저장.
- EnvironmentReference에는 outer 함수의 LexicalEnvironment가 담긴다. (this: 전역 객체)
- 4번째 줄
- 식별자 a에 접근하고자 한다. 현재 활성화 상태인 inner 컨택스트의 environmentRecord에서 a를 검색한다. (할당된 값이 없으므로 undefined 출력)- 5번째 줄
- inner 스코프에 있는 변수 a에 3을 할당한다.
inner 실행 컨텍스트가 콜 스택에서 제거되고, 바로 아래의 outer 실행 컨텍스트가 다시 활성화되면서, 7번째 줄의 다음으로 이동한다.
식별자 a에 접근하고자 한다. 이때 자바스크립트 엔진은 활성화된 실행 컨텍스트의 LexicalEnvironment에 접근한다. 첫 요소의 environmentRecord에서 a가 있는지 찾아보고, 없으면 outerEnvironmentReference에 있는 environmentRecord로 넘어가는 식으로 계속해서 검색한다. (전역 LexicalEnvironment에 a가 있음)
outer 실행 컨텍스트가 콜 스택에서 제거되고, 바로 아래의 전역 컨텍스트가 다시 활성화되면서, 10번째 줄의 다음으로 이동한다.
식별자 a에 접근하고자 한다. 현재 활성화 상태인 전역 컨텍스트의 environmentRecord에서 a를 검색한다. (1 출력)
전역 컨텍스트가 콜 스택에서 제거되고 종료한다.
위의 예시에서 inner 함수 내부 a 변수를 선언했기 때문에 전역 공간에서 선언한 동일한 이름의 a 변수에는 접근할 수 없다. 이를 변수 은닉화라고 한다.
inner 함수 내부에서 a에 접근하려고 하면 무조건 스코프 체인 상의 첫 번째 인자, 즉 inner 스코프의 LexicalEnvironment부터 검색할 수밖에 없고 여기에 a 식별자가 존재하므로 스코프 체인 검색을 더 진행하지 않고 즉시 a를 반환하게 되는 것이다.
outer 함수를 지역변수로 선언하기 위해 외부에 x라는 함수를 하나 더 만든다면. outer 함수를 호출할 수 있는 영역은 오직 x 내부로 국한되고 x 함수 내부의 코드들은 전역 공간에서 접근할 수 없게 된다.