실행 컨텍스트란?
실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다. 자바스크립트는 어떤 실행 컨텍스트가 활성화되는 시점에 다음과 같은 일을 한다.
1. 선언된 변수를 위로 끌어올림 (=호이스팅)
2. 외부 환경 정보를 구성
3. this 값을 설정
1. 스택 vs 큐
실행 컨텍스트를 이해하기 위해서는 "콜 스택"에 대한 이해가 반드시 필요하다. 그 전에 "스택" 과 "큐"에 대해 살펴보면 아래와 같다.
스택은 바스캣, 큐는 원통과 같은 것이라고 생각하면 되고 스택은 LIFO, 큐는 FIFO이다.
2. 콜 스택 (call stack)
실행 컨텍스트란 실행할 코드에 제공할 환경정보를 모아놓은 객체! 라고 했었는데 그 객체. 즉, 동일 환경에 있는 코드를 실행할 때 필요한 환경정보들을 모아 컨텍스트를 구성하고 이것을 위에서 설명한 스택의 한 종류인 콜 스택에 쌓아올린다.
가장 위에 쌓여있는 컨텍스트와 관련된 코드를 실행하는 방법으로 코드의 환경 및 순서를 보장할 수 있다.
가. 컨텍스트의 구성
구성 방법에는 여러가지가 있지만 함수만 생각하면 된다!
1. 전역공간
2. eval() 함수
3. ⭐️⭐️⭐️ 함수 → 우리가 흔히 실행 컨텍스트를 구성하는 방법
[실행 컨텍스트 예시 코드]
// 선언부
var a = 1;
function outer() {
function inner() {
console.log(a); //undefined
var a = 3;
}
inner(); // ---- 2번째로 실행
console.log(a);
}
outer(); // ---- 가장 먼저 실행
console.log(a);
위 코드는 아래 순서로 진행된다. (콜 스택에 쌓이는 실행 컨텍스트에 의해 순서가 보장되기 때문!)
코드 실행 → 전역(in) → 전역(중단) + outer(in) → outer(중단) + inner(in) → inner(out) + outer(재개) → outer(out) + 전역(재개) → 전역(out) → 코드 종료
결국은 특정 실행 컨텍스트가 생성되는 시점이 콜 스택의 맨 위에 쌓이는(노출되는) 시점을 의미하며, 현재 실행할 코드에 해당 실행 컨텍스트가 관여하게 되는 시점을 의미한다.
3. 실행 컨텍스트 객체의 실체 (= 담기는 정보)
VE와 LE는 같은 것이지만 변경사항을 실시간으로 반영하는지 여부에 차이가 있다.
this가 function안에서 어떤 역할을 하는지 결정해주는 역할을 하는 것이 this 바인딩이다.
⭐️ 가. VariableEnvironment
1) 현재 컨텍스트 내의 식별자 정보 (=record)를 갖고 있다.
var a = 3의 경우 var a를 의미한다.
2) 외부 환경 정보 (=outer)를 갖고 있다.
3) 선언 시점 LexicalEnvironment의 snapshot
⭐️ 나. LexicalEnvironment
1) VariableEnvironment 와 동일하지만 변경사항을 실시간으로 반영한다.
⭐️ 다. ThisBinding
1) this 식별자가 바라봐야할 객체
VariableEnvironment, LexicalEnvironment의 개요
1. VE vs LE
이 두가지는 담기는 항목은 완벽하게 동일하나
VE는 스냅샷을 유지하는 반면, LE는 스냅샷을 유지하지 않고 실시간으로 변경사항을 계속해서 반영한다.
⭐️ 결국, 실행 컨텍스트를 생성할 때, VE에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LE를 만들고 이후에는 주로 LE를 활용한다.
2. 구성요소 (VE, LE 동일)
environmentRecord와 outerEnvironmentReference로 구성된다.
environmentRecord는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장되며,
함수에 지정된 매개변수 식별자, 함수 자체, var로 선언된 변수 식별자 등이 저장된다.
outerEnvironmentReference는 외부 환경정보를 의미한다.
LexicalEnvironment - environmentRecord (= record)와 호이스팅
1. 개요
현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다.
저장 대상 정보로는 함수에 지정된 매개변수 식별자, 함수 자체, var로 선언된 변수 식별자 등이 있다.
컨텍스트 내부를 처음부터 끝까지 순서대로 훑어가며 수집한다. (수집이지 실행이 아님!)
2. 호이스팅
변수 정보 수집을 모두 마쳤더라도 아직 실행 컨텍스트가 관여할 코드는 실행 전의 상태이다.
(자바스크립트 엔진은 코드 실행 전 이미 모든 변수정보를 알고 있는 상태)
⭐️⭐️⭐️ 호이스팅은 변수 정보 수집 과정을 이해하기 쉽게 설명한 '가상 개념'이다.
1. 호이스팅 법칙 1 : 매개변수 및 변수는 선언부를 호이스팅한다.
2. 호이스팅 법칙 2 : 함수 선언은 전체를 호이스팅한다.
참고 ) 호이스팅
법칙 1 : 매개변수 및 변수는 선언부를 호이스팅한다.
// a라는 함수에 매개변수 x가 있다라고 가정하면 아래와 같이 풀어서 쓸 수 있다.
function a () {
var x = 1;
console.log(x);
var x;
console.log(x);
var x = 2;
console.log(x);
}
a(1);
// 위 코드에서 호이스팅을 적용해보면 다음과 같다.
function a () {
var x;
var x;
var x;
x = 1;
console.log(x);
console.log(x);
x = 2;
console.log(x);
}
a(1);
즉 호이스팅 전 코드를 보면 콘솔에 1 → undefined → 2 로 예상을 했지만
실제로는 1 → 1 → 2 라는 결과가 나왔다!!
법칙2 : 함수 선언은 전체를 호이스팅 한다.
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);
b = 'bbb'; // 변수의 할당부는 원래 자리에
console.log(b);
console.log(b);
}
a();
이번엔 에러 → 'bbb' → b 함수 로 예상했지만
실제로는 b함수 → 'bbb' → 'bbb' 라는 결과가 나왔다.
🔥🔥주의사항 : 함수 선언문과 함수 표현식에서 호이스팅 방식의 차이
먼저 함수를 정의하기 위한 방법에는 3가지가 있다.
1. 함수 선언문 : function 정의부만 존재, 할당 명령이 없는 경우
2. 함수 표현식 : 정의한 function을 별도 변수에 할당하는 경우
3. 기명 함수 표현식 : 변수명은 c, 함수명은 d와 같이 변수에 함수를 할당하는데도 불구하고 함수명을 정의하는 경우이나 잘 사용되지 x
// 함수 선언문. 함수명 a가 곧 변수명 // function 정의부만 존재, 할당 명령이 없는 경우 function a () { /* ... */ } a(); // 실행 ok // 함수 표현식. 정의한 function을 별도 변수에 할당하는 경우 // (1) 익명함수표현식 : 변수명 b가 곧 변수명(일반적 case에요) var b = function () { /* ... */ } b(); // 실행 ok // (2) 기명 함수 표현식 : 변수명은 c, 함수명은 d // d()는 c() 안에서 재귀적으로 호출될 때만 사용 가능하므로 사용성에 대한 의문 var c = function 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;
}
-------------호이스팅 적용-----------------
// 함수 선언문은 전체를 hoisting
function sum (a, b) { // 함수 선언문 sum
return a + b;
}
// 변수는 선언부만 hoisting
var multiply;
console.log(sum(1, 2));
console.log(multiply(3, 4));
multiply = function (a, b) { // 변수의 할당부는 원래 자리
return a + b;
};
[주의해야 하는 이유]
협업을 많이 하고 복잡한 코드일수록, 전역 공간에서 이루어지는 코드 협업일수록 함수 표현식을 활용하는 습관을 들여야 한다!!
여기까지가 호이스팅에 대한 내용이며, 다시 실행 컨텍스트 내용으로 돌아가기 위해 지금까지 알아본 것들을 정리하면 다음과 같다
- 실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.
- 그 객체 안에는 3가지가 존재한다.
✓ VariableEnvironment
✓ LexicalEnvironment
✓ ThisBindings
- VE와 LE는 실행컨텍스트 생성 시점에 내용이 완전히 같고, 이후 스냅샷 유지 여부가 다르다.
- LE는 다음 2가지 정보를 가지고 있다.
✓ record(=environmentRecord) ← 이 record의 수집과정이 hoisting
✓ outer(=outerEnvironmentReference)
LexicalEnvironment - 스코프, 스코프 체인, outerEnvironmentReference ( = outer)
1. 주요 용어
스코프 : 식별자에 대한 유효 범위를 의미한다.
스코프 체인 : 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것
outerEnvironmentReference (이하 outer) : 스코프 체인이 가능토록 하는 것 (외부 환경의 참조 정보)이라고 할 수 있다.
2. 스코프 체인
1. outer는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다. (= 실행컨텍스트가 만들어질 때의 그 환경정보를 가지고 있다)
2. 예를 들어 A함수 내부에 B함수 선언 → B함수 내부에 C함수 선언 한 경우 타고 올라가다 보면 전역 컨텍스트의 LexicalEnvironment를 참조하게 된다.
3. 항상 outer는 오직 자신이 선언된 시점의 LexicalEnvironment를 참조하고 있으므로 가장 가까운 요소부터 차례대로 접근 가능하다.
4. 결론 : 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에게만 접근이 가능
5. 예시 코드
// 아래 코드를 call stack을 그려가며 scope 관점에서 변수에 접근 var a = 1; var outer = function() { var inner = function() { console.log(a); // undefined -- 이유는 var a; console.log(a); a=3 이렇게 되기때문 var a = 3; }; inner(); console.log(a); // inner는 이미 콜스택에서 날아갔기 때문에 여기서 참조할 수 있는 데이터는 전역영역에서만 가져올 수 있다. 즉, a는 1이 찍힌다. }; outer(); console.log(a); // 전역 영역에서 할당한 1이 나온다.
3. 결론
즉, 각각의 실행 컨텍스트는 LE 안에 record와 outer를 가지고 있고 outer안에는 그 실행 컨텍스트가 선언될 당시의 LE 정보가 다 들어있으니 scope chain에 의해 상위 컨텍스트의 record를 읽어올 수 있다!!