새오의 개발 기록

코어 자바스크립트: 2. 실행 컨텍스트 본문

Javascript/코어 자바스크립트

코어 자바스크립트: 2. 실행 컨텍스트

새오: 2022. 12. 23. 16:41

02. 실행 컨텍스트

1. 실행 컨텍스트란?

 

실행할 코드에 제공할 환경 정보들을 모아놓은 객체

 

  • 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜 스택에 쌓아올렸다가, 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장함.
  • 동일한 환경: 하나의 실행 컨텍스트를 구성할 수 있는 방법으로 전역공간, eval()함수, 함수 등이 있음
    • 자동 생성되는 전역공간과 eval을 제외하면 실행 컨텍스트를 구성하는 방법은 함수 실행 뿐임

 

 

실행 컨텍스트와 콜 스택

// ---------------------------(1)
var a = 1;
function outer() {
	function inner() {
    	console.log(a); // undefined
        var a = 3;
    }
    inner(); // ---------------------------(2)
    console.log(a);
}
outer(); // ---------------------------(3)
console.log(a);

 

  1. 처음 자바스크립트 코드를 실행
    • (1) 전역 컨텍스트가 콜 스택에 담김
    • 콜 스택에 전역 컨텍스트 외에 다른 덩어리가 없으므로 전역 컨텍스트와 관련된 코드들을 순차로 진행
  2. (3) 에서 outer 함수를 호출
    • outer 실행 컨텍스트를 생성한 후 콜 스택에 담음
    • 전역 컨텍스트와 관련된 코드의 실행을 일시중단하고 outer 실행 컨텍스트와 관련된 코드들을 순차로 실행
  3. (2)에서 inner 함수를 호출
    • outer 컨텍스트와 관련된 코드의 실행을 일시중단하고 inner 실행 컨텍스트와 관련된 코드들을 순차로 실행
    • inner 함수 내부에서 a 변수에 값 3을 할당하고 나면 inner 함수의 실행이 종료되면서 inner 실행 컨텍스트가 콜 스택에서 제거됨
  4. 아래에 있던 outer 컨텍스트가 콜 스택의 맨 위에 존재하게 되므로 중단했던 (2)의 다음 줄부터 이어서 실행
    • a 변수의 값을 출력하고 나면 outer 함수의 실행이 종료되어 outer 실행 컨텍스트가 콜 스택에서 제거됨
  5. (3)의 다음 줄부터 이어서 실행
    • a의 변수의 값을 출력하고 나면 전역 공간에 더는 실행할 코드가 남아 있지 않아 전역 컨텍스트도 제거됨
  6. 콜 스택에는 아무것도 남지 않은 상태로 종료

 

 

 

이처럼 어떤 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는 데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장되며 이 객체에 담기는 정보는 아래와 같음


  • VariableEnvironment: 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보. 선언 시점의 LexicalEnvironment의 스냅샷으로, 변경사항은 반영되지 않음
  • LexicalEnvironment: 처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨
  • ThisBinding: this 식별자가 바라봐야 할 대상 객체
  • VariableEnvironment와 LexicalEnvironment의 내부는 environmentRecord와 outerEnvironmentReference로 구성됨

 


 

2. VariableEnvironment

 

  • VariableEnvironment에 담기는 내용은 LexicalEnvironment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 다름
  • 실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용

 


 

3. LexicalEnvironment

 

environmentRecord와 호이스팅

  • environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됨
    • 식별자 정보: 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언한 함수가 있을 경우 그 함수 자체, var로 선언된 변수의 식별자 등
  • 컨택스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집함
  • 변수 정보를 수집하는 과정을 마치고 나면 코드가 실행되기 전임에도 자바스크립트 엔진이 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있음 -> 자바스크립트 엔진은 식별자들을 최상단으로 끌어 올려놓은 다음 실제 코드를 실행함과 같음 -> 호이스팅

 

호이스팅 규칙

 

매개 변수와 변수 선언에 대한 호이스팅

// 호이스팅 전

function a (x) { // 수집 대상 1(매개변수)
	console.log(x);
    var x; // 수집 대상 2(변수 선언)
    console.log(x);
    var x = 2; 수집대상 3(변수 선언)
    console.log(x);
}
a(1);

 

  • environmentRecord는 현재 실행될 컨텍스트의 대상 코드 내에서 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 둠
  • 매개변수도 마찬가지로 동작함

 

// 호이스팅을 마친 상태

function a () {
	var x; // 수집 대상 1의 변수 선언 부분
    var x; // 수집 대상 2의 변수 선언 부분
    var x; // 수집 대상 3의 변수 선언 부분
    
    x = 1; // 수집 대상 1의 할당 부분
    console.log(x); // (1)
    console.log(x); // (2)
    x = 2; // 수집 대상 2의 할당 부분
    console.log(x); // (3)
}
a(1);

 

 

함수 선언에 대한 호이스팅

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();

 

  • a 함수를 실행하는 순간 a 함수의 실행 컨텍스트가 생성됨
    • 이때 변수명과 함수 선언의 정보를 위로 끌어올림
    • 변수는 선언부와 할당부를 나누어 선언부만 끌어올리는 반면 함수 선언은 함수 전체를 끌어올림

 

// 함수 선언문을 함수 표현식으로 바꾼 코드

function a () {
	var b;
    var b = function b () { }
    
    console.log(b); // (1)
    b = 'bbb';
    console.log(b); // (2)
    console.log(b); // (3)
}
a();

 

  • 호이스팅이 끝난 상태에서의 함수 선언문은 함수명으로 선언한 변수에 함수를 할당한 것처럼 여길 수 있음
  • (1) 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(); // 에러

 

  • 함수 선언문: function 정의부만 존재하고 별도의 할당 명령이 없는 것
    • 함수명이 반드시 정의되어 있어야 함 -> 기명 함수 표현식
    • 기명 함수 표현식은 외부에서는 함수명으로 함수를 호출할 수 없으며 함수 내부에서만 접근 가능함(ex. function d)
  • 함수 표현식: 정의한 function을 별도의 변수에 할당하는 것을 말함
    • 함수명을 반드시 정의할 필요 없음 -> 주로 익명 함수 표현식 의미

 

 

함수 선언문의 호이스팅

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;
}
// 호이스팅 마친 상태

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;
}

 

함수 선언문의 위험성

  • 전역 컨텍스트가 활성화될 때 전역공간에 선언된 함수들이 모두 가장 위로 끌어올려짐.
  • 동일한 변수명에 서로 다른 값을 할당할 경우 나중에 할당한 값이 먼저 할당한 값을 덮어씌움(override)
  • 의도한 값과 다른 값이 할당될 수 있음 -> 에러 발생되지 않음
  • 함수표현식으로 정의하는 경우 함수를 선언한 줄보다 이전 줄에 함수를 호출하면 에러가 발생해서 디버깅 하기 쉬움

 

스코프, 스코프 체인, outerEnvironmentReference

  • 스코프: 식별자에 대한 유효범위
  • 스코프 체인: 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것
  • 이를 가능케 하는 것이 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference

 

 

스코프 체인

  • outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조함
  • 선언이 일어날 수 있는 시점이란 콜 스택 상에서 어떤 실행 컨텍스트가 활성화된 상태일 뿐
    • ex) A 함수 내부에 B 함수를 선언하고 다시 B 함수 내부에 C 함수를 선언한 경우, 함수 C의 outerEnvironmentReference는 함수 B의 LexicalEnvironment를 참조함.
    • 함수 B의 LexicalEnvironment에 있는 outerEnvironmentReference는 다시 함수 B가 선언되던 때(A)의LexicalEnvironment를 참조
  • 이처럼 outerEnvironmentReference는 연결리스트 형태임
  • 선언 시점의 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 } 식별자를 저장
    • 전역 컨텍스트는 선언 시점이 없으므로 전역 컨텍스트의 outerEnverinmontReference에는 아무것도 담기지 않음
  • 1번째 줄과 2번째 줄
    • 전역 스코프에 있는 변수 a에 1을, outer에 함수를 할당
  • 10번째 줄
    • outer 함수 호출
    • 전역 컨텍스트의코드는 10번째 줄에서 임시중단되고, outer 실행 컨텍스트가 활성화되어 2번째 줄로 이동
  • 2번째 줄
    • outer 실행 컨텍스트의 environmentRecord에 { inner } 식별자 저장
    • outer 함수는 전역 공간에서 선언됐으므로 전역 컨텍스트의 LexicalEnvironment를 참조복사함
    • 이를 [ GLOBAL, { a, outer } ] 라고 표기.
    • 첫 번째는 실행 컨텍스트의 이름, 두 번째는 environmentRecord 객체(this: 전역 객체)
  • 3번째 줄
    • outer 스코프에 있는 변수 inner에 함수를 할당
  • 7번째 줄
    • inner 함수를 호출
    • outer 실행 컨텍스트의 코드는 7번째 줄에서 임시 중단
    • inner 실행 컨텍스트가 활성화되어 3번째 줄로 이동
  • 3번째 줄
    • inner 실행 컨텍스트의 environmentRecord에 { a } 식별자를 저장
    • inner 함수는 outer 함수 내부에서 선언됐으므로 outer 함수의 LexicalEnvironment, [ outer, { inner} ]를 참조 복사
  • 4번째 줄
    • 식별자 a에 접근
    • 현재 활성화 상태인 inner 컨텍스트의 environmentRecord에서 a를 검색 -> undefined
  • 5번째 줄
    • inner 스코프에 있는 변수 a에 3을 할당
  • 6번째 줄
    • inner 함수 실행이 종료됨
    • inner 실행 컨텍스트가 콜 스택에서 제거되고, 바로 아래의 outer 실행 컨텍스트가 다시 활성화되면서, 앞서 중단했던 7번째 줄의 다음으로 이동함
  • 8번째 줄
    • 식별자 a에 접근하고자 함
    • 첫 요소의 e.r에서 a가 있는지 찾아보고, 없으면 outerEnvironmentReference에 있는 e.r로 넘어가는 식으로 계속 검색
    • 전역 LexicalEnvironment에 a가 있으니 a에 저장된 값 1을 반환
  • 9번째 줄
    • outer 함수 실행이 종료됨
    • outer 실행 컨텍스트가 콜 스택에서 제거되고, 바로 아래의 전역 컨텍스트가 다시 활성화되면서, 앞서 중단했던 10번째 줄의 다음으로 이동함
  • 11번째 줄
    • 식별자 a에 접근하고자 함
    • 현재 활성화 상태인 전역 컨텍스트의 environmentRecord에서 a를 검색함 -> 1 출력
    • 모든 코드 실행 완료. 전역 컨텍스트가 콜 스택에서 제거되고 종료됨

 

  • 변수 은닉화
    • 스코프 체인 상에 있는 변수라고 해서 무조건 접근 가능한 게 아님
    • inner 함수 내부에서 a에 접근하려고 하면 무조건 스코프 체인 상의 첫 번째 인자, 즉 inner 스코프의 LexicalEnvironment부터 검색할 수밖에 없음
    • inner 스코프에 a가 있으면 전역 공간의 a 변수에는 접근할 수 없음

 

 

 

전역변수와 지역변수

 

  • 전역 변수: 전역 공간에서 선언한 변수
  • 지역 변수: 함수 내부에서 선언한 변수
  • 가급적 전역변수 사용을 최소화하고자 노력하는 것이 좋음

 

4. this

 

  • 실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장됨
  • 실행 컨텍스트 활성화 당시에 this가 지정되지 않은 경우 this에는 전역 객체가 저장됨
  • 그 밖에는 함수를 호출하는 방법에 따라 달라짐