본 포스팅은 SSAFYcial 10기로서 포스팅하는 개인적인 견해임을 밝힙니다. 😄
실행 컨텍스트를 살펴보기에 앞서 스택(stack)과 큐(queue)의 개념을 잠깐 살펴보자.
스택(stack): LIFO(Last In First Out)
큐(queue): FIFO(First In First Out)
실행 컨텍스트(execution context): 실행할 코드에 제공할 환경 정보들을 모아놓은 객체
동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜 스택(call stack)에 쌓아올렸다가, 가장 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.
하나의 실행 컨텍스트를 구성할 수 있는 방법으로 전역공간, eval()함수, 함수 등이 있다.
// ------------------- (1)
var a = 1;
function outer() {
function inner() {
console.log(a);
var a = 3;
// --------------- (2)
}
inner(); // -------- (3)
console.log(a);
// ----------------- (4)
}
outer(); // ---------- (5)
console.log(a);
// ------------------- (6)
//실행순서: (1) - (5) - (3) - (2) - (4) - (6)
실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용하게 된다.
VariableEnvironment: 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보, 선언 시점의 LexicalEnvironment의 snapshot
LexicalEnvironment: 처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨.
environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집한다. 변수 정보를 수집하는 과정을 모두 마쳤더라도 아직 실행 컨텍스트가 관여할 코드들은 실행되기 전의 상태이다.
호이스팅(hoisting)이란 변수 정보를 수집하는 과정을 더욱 이해하기 쉬운 방법으로 대체한 가상의 개념이다. (자바스크립트 엔진이 실제로 끌어올리지는 않음)
//매개변수와 변수에 대한 호이스팅 - 원본코드
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)
지금부터 자바스크립트 엔진의 구동 방식을 좀 더 사람의 입장에서 이해해보고자 코드를 변경할 것이다. 실제 엔진은 이런 변환 과정을 거치지 않는다. 오해하지 않을 것.
//매개변수와 변수에 대한 호이스팅 - 호이스팅을 마친 상태
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 (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();
//함수 선언의 호이스팅 - 호이스팅을 마친 상태
function a (){
var b; //수집 대상1 변수는 선언부만 끌어올린다.
function b () { } //수집 대상2 함수 선언은 전체를 끌어올린다.
console.log(b); //(1)
b = 'bbb'; //변수의 할당부는 원래 자리에 남겨둔다.
console.log(b); //(2)
console.log(b); //(3)
}
a();
위 코드의 결과 (1) function b (2) 'bbb' (3) 'bbb' 라는 결과가 나온다.
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){ //함수 선언문 sum
return a + b;
}
var multiply = function(a, b){ //함수 표현식 multiply
return a * b;
}
//함수 선언문과 함수 표현식 - 호이스팅을 마친 상태
var sum = function sum(a, b){ //함수 선언문은 전체를 호이스팅한다.
return a + b;
}
var multiply; //변수는 선언부만 끌어올린다
console.log(sum(1, 2)); //(1)
console.log(multiply(3, 4)); //(2)
multiply = function(a, b){ //변수의 할당부는 원래 자리에 남겨둔다.
return a * b;
}
(1)의 결과 3이 정상적으로 출력되고, (2)의 결과로는 'multiply is not a function'이라는 에러 메세지가 출력된다.
상대적으로 함수 표현식이 안전하므로 함수 선언문보단 함수 표현식 사용을 지향할 것.
스코프(scope)란 식별자에 대한 유효범위이다.
이러한 '식별자의 유효범위'를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인(scope chain)이라고 한다. 그리고 이를 가능케 하는 것이 바로 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference이다.
outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다. 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근이 가능하게 된다.
예제로 살펴보자.
var a = 1;
function outer() {
function inner() {
console.log(a);
var a = 3;
}
inner();
console.log(a);
}
outer();
console.log(a);
전역 공간에서는 전역 스코프에서 생성된 변수에만 접근할 수 있다.
outer 함수 내부에서는 outer 및 전역 스코프에서 생성된 변수에 접근할 수 있지만 inner 스코프 내부에서 생성된 변수에는 접근하지 못한다.
inner 함수 내부에서는 inner, outer, 전역 스코프 모두에 접근할 수 있다.
하지만 스코프 체인 상에 있는 변수라고 해서 무조건 접근 가능한 것은 아니다.
위 코드 상의 식별자 a는 전역공간에서도 선언했고 inner함수 내부에서도 선언했다. inner함수 내부에서 a에 접근하려고 하면 무조건 스코프 체인 상의 첫번째 인자인 LexicalEnvironment부터 검색할 수 밖에 없다. inner 스코프의 LexicalEnvironment에 a 식별자가 존재하므로 스코프 체인 검색을 더 진행하지 않고 inner LexicalEnviornment의 a를 반환하여 undefined를 반환한다.
즉, inner함수 내에서 a변수를 선언했기 때문에 전역변수의 a변수에는 접근할 수 없는 것이다. 이를 변수 은닉화(variable shadowing)이라고 한다.
전역변수(global variable): 전역 공간에서 선언한 변수, 전역 컨텍스트의 LexicalEnvironment에 담긴 변수
지역변수(local variable): 함수 내부에서 선언한 변수, 함수에 의해서 생성된 실행 컨텍스트의 변수
안전한 코드 작성을 위해 전역변수의 사용은 지양하도록 하자.
실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다. 실행 컨텍스트 활성화 당시에 this가 지정되지 않은 경우 this에는 전역객체가 저장된다.
실행 컨텍스트: 실행할 코드에 제공할 환경 정보들을 모아놓은 객체
호이스팅: 코드 해석을 수월하게 하기 위해 environmentRecord의 수집 과정을 추상화한 개념
스코프: 변수의 유효범위
전역변수: 전역 컨텍스트의 LexicalEnvironment에 담긴 변수
지역변수: 그 밖의 함수에 의해 생성된 실행컨텍스트의 변수
this: 실행 컨텍스트를 활성화하는 당시에 지정된 this