렉시컬 환경, 클로저... 이것들은 다 뭐랑 관련된걸까?
그렇다면 코어 자바스크립트 선생님께 실행 컨텍스트를 여쭤보자구요~!
(광고 아닙니다.. 책이 진짜 좋아서 그런고임....)
JS 엔진이 코드를 실행하기 위해 필요한 정보를 수집하는데,
이러한 정보들을 모아놓은 객체를 실행 컨텍스트
라 한다.
실행 컨텍스트
는 동일한 환경(스코프)에 있는 코드들을 실행할 때 필요한 환경 정보
들을 모아
하나의 컨텍스트를 구성하고, 이를 콜스택 안에 쌓아올렸다가 실행해서 pop하는 식이다.
실행 컨텍스트
는
전역 코드
(전역 영역에 존재하는 코드)eval 코드
(eval 함수로 실행되는 코드) 함수 코드
(함수 내에 존재하는 코드) ⭐️ 에 의해 생성되어 콜스택에 쌓이는데,
전역 코드
는 JS 파일이 실행되면 자동으로 생성되므로 넘어가고,
보통은 함수가 실행될 때 하나의 컨텍스트가 생성되는 것으로 기억하는 것이 좋겠다.
eval은 왜 따로 컨텍스트가 생성될까?
예시를 통해 알아보자.
// (1) 전역 컨텍스트
let a = 1;
function outer() {
function inner() {
console.log(a);
let a = 3;
}
inner(); // (3) inner 함수 실행
console.log(a);
}
outer(); // (2) outer 함수 실행
console.log(a);
JS 파일을 실행하면 전역 컨텍스트
가 콜스택에 담긴다.
현재 콜스택에는 전역 컨텍스트
밖에 없으므로
전역 컨텍스트와 관련된 코드들을 순차적으로 진행한다.
outer()
을 만나면 JS 엔진이 outer 함수에 대한 환경 정보를 수집해
outer 실행 컨텍스트를 생성하고 콜스택에 담는다.
콜스택 맨 위부터 우선 실행하므로 전역 컨텍스트
관련 코드 실행을 일시중단 후,
outer 컨텍스트
=> outer 함수 내부 코드들을 순차적으로 실행한다.
inner()
를 만나면 inner 함수 실행 컨텍스트
가 콜스택 맨 상단에 쌓이고
outer 컨텍스트
관련 코드들을 실행 중단 후 inner 함수
내부 코드를 순서대로 진행한다.
inner 함수
내부 코드들이 다 실행되고 나면
inner 실행 콘텍스트
가 콜스택에서 제거된다.
콜스택 맨 상단에 outer 컨텍스트
가 있으므로,
일시중단했던 (3)의 다음 줄부터 이어서 실행한다.
outer 함수
내부의 코드들이 다 실행되고 나면
outer 컨텍스트
가 콜스택에서 제거되고 전역 컨텍스트
만 남는다.
console.log(a)
가 실행되고나면
전역 공간에 실행할 코드가 없기 때문에 전역 컨텍스트
가 제거되고
콜스택이 빈 채로 종료된다.
실행 컨텍스트
는 동일한 환경(스코프)에 있는 코드들을 실행할 때
필요한환경 정보
들을 모아 하나의 컨텍스트를 구성한다.
환경 정보
는 실행 컨텍스트가 생성될 때 수집된 정보로,
variableEnvironment
LexicalEnvironment
ThisBinding
으로 구성된다.
variableEnvironment
실행 컨텍스트 생성 시 variableEnvironment
에 정보를 먼저 담고,
복사 해 LexicalEnvironment
를 만든다.
이후부턴 LexicalEnvironment
를 주로 활용한다.
초기화 과정 중엔 둘이 완전히 동일하기 때문에 구체적인 것은 LexicalEnvironment
에서 살펴보자.
LexicalEnvironment
variableEnvironment
와 LexicalEnvironment
내부는
enviromentRecord
(식별자 정보)outer-LexicalEnvironment
(외부환경 정보)enviromentRecord
는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 이 객체에 프로퍼티로 저장된다.
예를 들어 함수가 실행되며 컨텍스트가 구성될 텐데, 이 실행 함수에 지정된
매개변수 식별자
,
함수 선언식
으로 선언된 함수 자체 (함수 표현식 제외),
var
로 선언된 변수의 식별자 등이 저장된다.
무언가 떠오르지 않는가?
함수 선언식
으로 선언된 함수와, var
로 선언된 변수는
호이스팅에 의해 코드 상단부로 끌어올려진다는 것...!
호이스팅이 바로 실행 컨텍스트의
Lexical(variable)Environment
의 enviromentRecord
에 의해 일어나는 일이다 🙀.
실제로는 끌어올려지는 것이 아닌 enviromentRecord
가
식별자 정보를 수집하며 환경 정보를 구성하는 것으로,
코드가 실행되기 전에 환경 정보가 수집되어
JS 엔진이 해당 Environment에 속한 변수명들을 알고 있는 셈이다.
function a () {
console.log(b); // (1)
var b = 'bbb';
console.log(b); // (2)
function b() {}
console.log(b); // (3)
}
a();
😎 과정 설명
1. a함수
가 실행되며 a함수의 실행 컨텍스트
가 생성된다.
2. 이 때 enviromentRecord
에 식별자 정보가 수집된다.
⚠️ 식별자 정보를 수집할 땐 변수 선언부
(할당부 X)와 함수 선언식
의 정보를 수집한다.
변수는 선언부만 수집하지만, 함수 선언은 함수 전체를 수집한다.
수집하는 과정을 아래와 같이 표현했으나, 실제로 저렇게 변하는 것은 아니다.
3. var 변수의 선언부
와 함수 선언식으로 선언된 함수
의 정보가 수집되었다.
이때 함수 선언식 함수
는 호이스팅 시,
함수명으로 선언된 변수에 함수가 할당된 것으로 생각할 수 있다.
4. var b
변수에 함수 b
가 할당됐기 때문에 (1)엔 b함수
가 출력될 것이다.
b에 'bbb'가 할당된 직후 (2), (3)엔 bbb가 출력된다.
5. 함수 내부에 모든 코드가 실행되어서 a 함수의 실행 컨텍스트
가 콜스택에서 제거된다.
함수 선언식: function b () {};
형태
함수 표현식: 변수 b = function () {};
형태 👉🏻 (익명) 함수 표현식
함수 선언식
은 호이스팅 과정에서 environmentRecord
에 함수가 통채로 수집된다.
반면 함수 표현식
은 변수에 값으로써 함수가 할당된 형태로,
변수 선언부만 수집되고 나머지 할당부는 수집되지 않는다.
결과적으로, 함수 표현식을 선언 전에 호출하면
선언부만 수집되었기 때문에 호출이 불가하다고 나올 것이다.
스코프
: 식별자에 대한 유효범위.
스코프체인
: 스코프를 안에서부터 바깥으로 차례로 검색해나가는 것.
outer-EnvironmentReference
에 저장된
상위 스코프의LexicalEnvironment
를 탐색하면서 스코프체인이 가능도록 한다.
때문에 outer-EnvironmentReference
은 연결리스트로 되어있다.
outer-EnvironmentReference
는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment
를 참조한다.
말이 어렵기 때문에, 실제 코드를 보며 해석해보자.
⚠️ 그림 속 약자는 아래의 축약 의미이다.
L.E
= LexicalEnvironment
e
= environmentRecord
o
= outerEnvironmentReference
// 전역 컨텍스트 활성화
var a = 1;
var outer = function () {
var inner = function () {
console.log(a);
var a = 3;
};
inner();
console.log(a);
};
outer();
console.log(a);
JS 파일이 실행되며 자동으로 전역 컨텍스트
가 콜스택에 생성된다(활성화).
전역 컨텍스트의 environmentRecord(e)
에 {a, outer}
식별자가 저장된다.
전역 컨텍스트는 선언 시점이 없으므로
전역 컨텍스트의 outerEnvironmentReference(o)
엔 아무것도 안 담긴다.
JS엔진이 환경 정보를 수집한 후 위에서 아래로 코드를 실행한다.
전역 스코프에 있는 변수 a에 1을, outer에 함수를 할당한다.
outer()
을 실행한다. 함수가 실행되면 컨텍스트가 콜스택에 생성(활성화)되고,
환경 정보를 수집하기 위해 outer 함수 내부 코드를 살핀다.
outer 컨텍스트의 environmentRecord(e)
에 {inner}
식별자가 저장된다.
outerEnvironmentReference(o)
엔
outer 함수가 선언될 당시의 LexicalEnvironment
가 담긴다.
outer 함수는 전역 공간에서 선언됐으므로
전역 컨텍스트의 LexicalEnvironment
를 참조복사한다.
4. outer 스코프
에 있는 inner 변수에 함수를 할당한다.
5. inner 함수
를 호출한다.
마찬가지로 outer 함수
의 코드 실행이 일시 중단되고,
콜스택에 inner 실행 컨텍스트
가 활성화된다.
inner 컨텍스트
의 environmentRecord(e)
에 {a}
식별자가 저장된다.
outerEnvironmentReference(o)
엔 inner 함수
가 선언될 당시의
LexicalEnvironment
가 담긴다.
inner 함수는 outer 함수 내부에서 선언됐으므로
outer 함수의 LexicalEnvironment
를 참조복사한다.
6. console.log(a)
에서 a에 접근하려 하는데,
inner 컨텍스트
의 environmentRecord(e)
에 a를 찾는다.
선언부는 있으나 값은 없으므로 undefined
출력.
inner
스코프에 있는 변수 a에 3을 할당한다.
7. inner 함수
실행이 끝났으므로 콜스택에서 inner 컨텍스트
가 제거되고,
outer 실행 컨텍스트
가 활성화되며 일시중단됐던 다음 코드부터 실행한다.
8. console.log(a)
에서 a에 접근하고자
outer 컨텍스트의 environmentRecord(e)
에서 a를 찾는다.
a 식별자가 없으므로 전 전역 EnvironmentReference(o)
의 L.E에서 찾는다.
a가 있으니 a에 저장된 값 1을 반환한다.
9. outer 함수 실행이 종료됐으므로, 콜스택에서 삭제되고
전역 컨텍스트가 활성화되며 일시중단됐던 다음코드, console.log(a)
를 실행한다.
전역 컨텍스트의 environmentRecord(e)
에서 a를 찾고, 있으므로 1을 반환한다.
10. 모든 함수 실행이 종료됐으므로 콜스택이 비워진 채로 끝이난다.
ThisBinding
실행 컨텍스트의 thisBinding
엔 this로 지정된 객체가 저장된다.
실행 컨텍스트가 활성화(함수 호출) 당시에 this가 지정되지 않았을 경우,
this는 바로 전역 객체가 된다.
가깝지만 먼 당신... this에서 자세히 정리했다!!
이제 테스형도 실행 컨텍스트를 마스터 하였다. 따봉 👍
와 오늘도 실행 컨텍스트에게 정복당할 뻔했다. 생각보다 만만치 않았다...
코드로만 읽으려니 머릿속에 추상적으로 남아있어 그림으로 하나씩 정리를 했는데, 역시 쏙쏙 들어오는 것이 감칠맛이 난다. 하지만 시간은 두배가 돼...