실행 컨텍스트
실행 컨텍스트란 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로,
자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념이다.
자바스크립트는 어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어올리고(호이스팅),
외부 환경 정보를 구성하고
this값을 설정하는 등의 동작을 수행한다.
=> 자바스크립트가 실행되기 위한 변수, 스코프 등을 묶어서 말하는 것.
1) Global execution context : global로 자바스크립트가 실행되는 환경, 무조건 하나만 존재할 수 있음
2) Functional execution context : 호출된 함수가 실행될때 생성되는 execution context
3) Eval : 극히 드문 경우
그리고 또 이로 인해 다른 언어와는 다른 특이 현상이 생겨나게 된다.
- 스텍: 출입구가 하나인 데이터 구조
- 큐: 양쪽이 모두 열린 데이터 구조. 한쪽은 입력만, 한 쪽은 출력만 가능하다.
동일한 환경(하나의 실행 컨텍스트를 구성할 수 있는 방법)에 있는 코드들을 실행할 때,
필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜스택에 쌓아올렸다가,
가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.
흔히 컨텍스트를 구성하는 방법으로 함수를 실행하는 것이 있다.
//-----------------------------(1)
var a = 1;
function outer(){
function inner(){
console.log(a); //undefined
var a = 3;
}
inner(); //----------------(2)
console.log(a); //1
}
outer(); //------------------(3)
console.log(a); //1
(1) 자바스크립트가 코드를 실행하는 순간(js파일이 열리는 순간) 전역 컨텍스트가 콜스택에 담긴다.
(전역 컨텍스트는 실행 컨택스트와 별다를 것은 없다.)
콜스택에는 전역 컨텍스트 외 다른 덩어리가 없으므로 전역 컨텍스트 관련 코드들을 순차로 진행하다가,
(3)에서 outer함수를 호출하면, 자바스크립트 엔진은 outer에 대한 환경 정보를 수집해서 outer실행 컨텍스트를 생성한 후 콜스택에 담는다.
콜스택의 맨 위에 outer 실행 컨텍스트가 놓인 상태가 되었으므로 전역 컨텍스트와 관련된 코드의 실행을 일시중단하고 대신 outer실행 컨텍스트와 관련된 코드, 즉 outer함수 내부의 코드들을 순차로 실행한다.
다시 (2)에서 inner함수의 실행 컨텍스트가 콜스택의 가장 위에 담기면 outer컨텍스트와 관련된 코드의 실행을 일시중단하고 inner함수 내부의 코드 순서대로 진행한다.
inner함수 내부에서 a변수에 3값을 할당하고 나면, inner함수의 실행이 종료되며 inner실행 컨텍스트가 콜스택에서 제거된다.
그 뒤, 아래의 outer컨텍스트가 콜스택의 맨 위에 존재하게 되므로 중단했던 (2) 다음 줄부터 이어서 실행한다.
실행이 끝나면 outer 실행 컨텍스트도 콜스택에서 제거되고
콜스택에는 전역 컨텍스트만 남아있게 된다.
그 다음, 실행을 중단했던 (3)이후부터 실행하고 a값을 출력하고 나면 전역 공간에 더는 실행 코드가 없어 전역 컨텍스트도제거되고 콜스택에는 아무것도 남지 않은 상태로 종료된다.
이와 같이 어떤 실행 컨텍스트가 활성화될때,
자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는데 필요한 환경 정보들을 수집해 실행 컨텍스트 객체에 저장한다.
여기에 담기는 정보는 다음과 같다.
- VariableEnvironment
- LexicalEnvironment
- ThisBinding
VariableEnvironment
LexicalEnvironment과 담기는 내용은 같지만, 최초 실행 시의 스냅샷을 유지한다는 점이 다르다.
실행 컨텍스트를 생성할 때, VariableEnvironment에 정보를 먼저 담은 다음,
이를 그대로 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용한다.
VariableEnvironment와 LexicalEnvironment의 내부는 enviromentRecord와 outer-EnvironmentReference로 구성되어 있다.
LexicalEnvironment
enviromentRecord와 호이스팅
enviromentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다.
(컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언한 함수가 있을 경우 그 함수 자체, var로 선언된 변수의 식별자 등이 식별자에 해당함)
컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집한다.
변수 정보를 수집하는 과정을 모두 마쳤더라도 아직 실행 컨텍스트가 관여할 코드들은 실행되기 전의 상태이다. 코드가 실행되기 전임에도 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있게 되는 셈이다.
여기서 호이스팅이라는 개념이 등장한다.
호이스팅이란 끌어올리다라는 hoist에 ing를 붙여 만든 동명사로, 변수 정보를 수집하는 과정을 더욱 이해하기 쉬운 방법으로 대체한 가상의 개념이다. 자바스크립트 엔진이 실제로 끌어올리지는 않지만 편의상 끌어올린 것으로 간주하자는 것이다.
* 실행 컨텍스트 추가설명
https://www.youtube.com/watch?v=AbNc8_poxu4
호이스팅 규칙
호이스팅이 되지 않았을 때, (1), (2), (3)에서 어떤 값들이 출력될까?
function a(x) { // 수집 대상1(매개변수)
console.log(x); //(1)
var x; //수집 대상2(변수선언)
console.log(x); //(2)
var x = 2; //수집 대상3(변수선언)
console.log(x); //(3)
}
a(1)
(1)에서는 함수 호출 시 전달한 1이 출력되고,
(2)에는 선언된 변수 x에 할당 값이 없으므로 undefined가 출력되고,
(3)에서는 2가 출력될 것이라고 예측해 볼 수 있다.
과연 그럴까?
위의 예제에서 인자들과 함께 함수를 호출한 경우의 동작을 살펴보면, arguments에 전달된 인자를 담는 것을 제외하면 코드 내부에서 변수를 선언한 것과 다르지 않다. 특히 LexicalEnvironment 입장에서는 완전히 같다. 인자를 함수 내부의 다른 코드보다 먼저 선언 및 할당이 이뤄진 것으로 간주할 수 있다. 이런 방식으로 코드를 바꾸어보자.
//매개변수를 변수 선언/할당과 같다고 간주해서 변환한 상태
function a() {
var x = 1; // 수집 대상1(매개변수)
console.log(x); //(1)
var x; //수집 대상2(변수선언)
console.log(x); //(2)
var x = 2; //수집 대상3(변수선언)
console.log(x); //(3)
}
a()
이 상태에서 변수 정보를 수집하는 과정인 호이스팅을 처리해보자.
enviromentRecord는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 어떤 값이 할당될 것인지는 관심이 없다.
따라서, 변수를 호이스팅할때, 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 남겨둔다. 매개변수의 경우도 마찬가지다. 이에 맞춰 순서대로 수집대상을 끌어올리고 나면, 다음과 같은 형태로 바뀐다.
//호이스팅을 마친 상태
function a() {
var x; //수집대상 1의 변수 선언 부분
var x; //수집대상 2의 변수 선언 부분
var x; //수집대상 3의 변수 선언 부분
x = 1; // 수집 대상1의 할당 부분
console.log(x); //(1)
console.log(x); //(2)
x = 2; //수집 대상3의 할당 부분
console.log(x); //(3)
}
a(1);
처음에 우리는
(1)에서는 함수 호출 시 전달한 1이 출력되고,
(2)에는 선언된 변수 x에 할당 값이 없으므로 undefined가 출력되고,
(3)에서는 2가 출력될 것이라고 예측했다.
그런데 호이스팅을 마친 후 결과값을 보면, 실제로는
(1) 1,
(2) 1,
(3) 2
라는 결과값이 나왔다.
이 결과는 호이스팅을 이해하지 못하면 예측하기 어려운 결과이다.
함수 선언을 추가한 예제를 하나 더 살펴보자.
function a() {
console.log(b); //(1)
var b = 'bbb'; //수집대상 1(변수선언)
console.log(b); //(2)
function b() { } //수집대상 2(함수 선언)
console.log(b); //(3)
}
a();
출력 결과를 예측해보자.
(1)에는 b값이 없으니 에러가 나거나 undefined가 나오고,
(2)는 'bbb',
(3)은 b함수가 출력될 것이다.
실제로도 그럴까?
a함수를 실행하는 순간, a함수의 실행 컨텍스트가 생성된다.
이때, 변수명과 함수 선언의 정보를 위로 끌어올린다(수집한다.)
변수는 선언부와 할당부를 나누어 선언부만 끌어올리는 반면, 함수 선언은 함수 전체를 끌어올린다.
위와 같은 과정을 겪고 나면 다음과 같은 형태로 변한다.
//호이스팅을 마친 상태
function a() {
var b; //수집대상 1 변수는 선언부만 끌어올린다.
function b() { } //수집대상 2 함수 선언은 전체를 끌어올린다.
console.log(b); //(1)
b = 'bbb'; //변수의 할당부는 원래 자리에 남겨둔다.
console.log(b); //(2)
console.log(b); //(3)
}
a();
해석상 편의를 위해 한 가지 더 언급하자면, 호이스팅이 끝난 상태에서 함수 선언문은 함수명으로 선언한 변수에 함수를 할당한 것처럼 여길 수 있다.
즉 아래와 같아진다.
function a() {
var b; //수집대상 1 변수는 선언부만 끌어올린다.
var b = function b() { } //수집대상 2 함수 선언은 전체를 끌어올린다.
console.log(b); //(1)
b = 'bbb'; //변수의 할당부는 원래 자리에 남겨둔다.
console.log(b); //(2)
console.log(b); //(3)
}
a();
(1)에는 b값이 없으니 에러가 나거나 undefined가 나오고,
(2)는 'bbb',
(3)은 b함수가 출력될 것이라고 예측했지만,
실제 결과는 (1)b함수, (2)'bbb', (3)'bbb'라는 전혀 다른 결과가 나왔다.
함수 선언문과 함수 표현식
둘 다 모두 함수를 새롭게 정의할때 쓰이는 방식이다.
그 중 함수 선언문은 function정의부만 존재하고 별도의 할당 명령이 없는 것을 의미한다.
함수 선언문의 경우 반드시 함수명이 정의되어 있어야 한다.
함수 표현식은 정의한 function을 별도의 변수에 할당하는 것을 말한다.
함수명이 정의되어 있지 않아도 된다.
함수명을 정의한 함수 표현식을 기명 함수 표현식,
정의하지 않은 것을 익명 함수 표현식이라고 부른다. -> 일반적인 함수 표현식이다.
function a(){ } //함수 선언문, 함수명 a가 곧 변수명
a(); //실행 ok
var b = function() { } //익명함수표현식. 변수명 b가 곧 함수명
b(); //실행 ok
var c = function d(){ } //기명 함수 표현식. 변수명은 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
}
실행 컨택스트의 LexicalEnvironment는 두 가지 정보를 수집한다.
여기서는 enviromentRecord의 정보 수집 과정에서 발생하는 호이스팅을 살펴보고 있다.
호이스팅을 마친 상태를 확인해보자.
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
}
함수 선언문은 천체를 호이스팅했지만,
함수 표현식은 변수 일부만 호이스팅했다.
sum함수는 선언 전에 호출해도 실행되지만, multiply는 그렇지 않게 된다.
override: 동일한 변수명에 서로 다른 값을 할당할 경우, 나중에 할당한 값이 먼저 할당한 값을 덮어 씌운다.
-> 상대적으로 함수 표현식이 안전하다. (디버깅이 쉽다)
스코프, 스코프 체인, outer-EnvironmentReference
스코프scope란 식별자에 대한 유효범위이다. (대부분의 언어에 존재한다)
이 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인이라고 한다.
이를 가능케 하는 것이 바로 LexicalEnvironment의 두 번째 수집 자료인 outer-EnvironmentReference이다.
스코프체인
outer-EnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다.
예를 들어, A함수 내부에 B함수를 선언하고, 다시 B함수 내부에 C함수를 선언한 경우, 함수C의 outer-EnvironmentReference는 함수 B의 LexicalEnvironment를 참조한다. 함수 B의 outer-EnvironmentReference는 함수 A의 LexicalEnvironment를 참조한다.
이와 같이 outer-EnvironmentReference는 연결 리스트의 형태를 띈다.
이와 같은 구조적 특징 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근이 가능하게 된다.
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 } 식별자를 저장한다. 전역 컨텍스트에는 선언 시점이 없으므로 전역 컨텍스트의 outer-EnvironmentReference에는 아무것도 담기지 않는다.
변수 은닉화: inner함수 내부에서 a함수를 선언했기 때문에 전역 공간에서 선언한 동일한 이름의 a변수에는 접근할 수 없다.
전역변수와 지역변수
: 코드의 안전성을 위해 가급적 전역변수 사용 최소화 하는 것이 좋다.
this
실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 지정된다. 실행 컨텍스트 활성화 당시에 this가 지정되지 않은 경우, 전역 객체가 저장된다. 자세한 것은 3장에서 다룬다.
'자바스크립트 공부하기' 카테고리의 다른 글
[코어자바스크립트] 04. 콜백함수 (0) | 2021.06.22 |
---|---|
[코어자바스크립트] 03.this (0) | 2021.05.20 |
[코어자바스크립트] 1단원 정리 (0) | 2021.05.11 |
[코어자바스크립트] 6.undefined와 null (0) | 2021.05.11 |
[코어자바스크립트] 5.불변 객체 (0) | 2021.05.11 |