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

2021. 6. 20. 17:23개발공부/자바스크립트

Everything in JS happens inside an "Execution Context"

실행 컨텍스트 = 자바 스크립트가 실행되기 위해 필요한, 변수, 스코프, this값등의 묶음 = 코드가 실행되고 있는 구역, 범위

한마디로, Execution Context 박스 안에서 자바스크립트 코드들이 실행되는 것

실행 컨텍스트란?

이미지 출처: https://medium.com/@Adi_Wang1476/stack-and-queue-1823effb6cc

  • 스택: 출입구가 하나뿐인 깊은 우물 같은 데이터 구조(LIFO = 1 - 2 - 3 순으로 들어가서 3 - 2 - 1 순으로 나옴)
  • 큐: 양쪽이 모두 열려있지만 보통 한쪽은 입력, 다른 한쪽은 출력만을 담당하는 데이터 구조(FIFO = 1 - 2 - 3 순으로 들어가서 1 - 2 - 3순으로 나옴)

그래서 이 스택과 큐가 실행 컨텍스트와 무슨 상관일까?

➡️ 실행 컨텍스트란 실행할 코드에 제공할 환경 정보들을 모아놓은 객체라고 하였다. 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜 스택에 쌓아올렸다가, 가장 위에 쌓여있는 컨텍스트와 관련있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다. 

✋🏻 동일한 환경이란?

하나의 컨텍스트를 구성할 수 있는 방법으로 전역공간, eval() 함수, 함수 등이있는데 흔히 실행컨텍스트를 구성하는 방법은 함수를 실행하는 것이다. (전역공간은 자동생성)

Types of Execution Context

  1. Global Execution Context(GEC) = 전역공간
  2. Functional Execution Context(FEC) = 함수
  3. Eval() 함수 ⇒ 잘 사용되지않음
// --------- (1)
var a = 1;               // global context
function outer() {       
		function inner() {
			console.log(a);
		var a = 3;
	} 

	inner(); // --------- (2)
	console.log(a);
}
outer();                // functional context // --------- (3)
console.log(a);

1️⃣ 처음 자바스크립트 코드를 실행하는 순간(1) 전역 컨텍스트가 콜 스택에 담긴다.

2️⃣ 전역 컨텍스트와 관련된 코드들을 순차로 진행하다가 (3)에서 outer함수를 호출하면 자바스크립트 엔진은 outer에 대한 환경 정보를 수집해서 outer 실행 컨텍스트를 생성한 후 콜 스택에 담는다. 이제 outer 실행 컨텍스트가 콜 스택의 최 상단에 있으니 전역 컨텍스트와 관련된 코드의 실행을 일시중단하고 outer 함수 내부의 코드들을 순차로 실행한다. 

3️⃣ (2)에서 inner함수의 실행 컨텍스트가 콜 스택의 가장 위에 담기면 outer컨텍스트와 관련된 코드의 실행을 일시 중단 후 inner 함수 내부의 코드를 순서대로 진행한다.

4️⃣ inner 함수 내부에서 a변수에 값 3을 할당하고 나면 inner 함수의 실행이 종료되면서 inner 실행 컨텍스트가 콜 스택에서 제거된다. 이제 outer컨텍스트가 콜 스택의 맨 위에 존재하게 되므로 중단했던 (2)의 다음 줄부터 이어서 실행한다.(다음 줄???)

5️⃣ a변수의 값을 출력하고 나면 outer 함수의 실행이 종료되어 outer 실행 컨텍스트가 콜 스택에서 제거되고, 콜 스택에는 전역 컨텍스트만 남아있게 된다. 

6️⃣ 마지막으로, 실행을 중단했던 (3)의 다음 줄부터 실행하고, a변수의 값을 출력하고 나면 전역공간에 더는 실행할 코득가 남아있지않아 전역 컨텍스트도 제거되고, 콜스택에는 아무것도 남지않은 상태로 종료된다.

 

⇒ 이렇게 콜 스택의 최상단에 쌓인 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는 데 

필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장한다.

  • VariableEnvironment: key값, 함수 등을 담고있음(메모리 컴포넌트), 최초 실행시의 스냅샷 유지
      LE와 담기는 내용은 동일하지만 최초 실행 시의 스냅샷을 유지한다는 점이 다르다. 실행 컨텍스트를 생성할 때 VE에 정보를 먼저 담은 다음, 이를 그대로 복사해 LE를 만들고 이후에는 LE를 주로 활용한다.
  • LexicalEnvironment: 변경 사항이 실시간으로 반영됨(백과사전 같은 너낌,,)
      "현재 컨텍스트의 내부에는 a, b, c와 같은 식별자들이 있고 그 외부 정보는 D를 참조하도록 구성돼있다"라는, 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아놓은 것.
    *VariableEnvironment와 LexicalEnvironment의 내부는 environmentRecord와 outer-EnvironmentReference로 구성되어있다.
  • ThisBinding: this 식별자가 바라봐야 할 대상 객체

 

environmentRecord와 호이스팅

  • envrionementRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다.
  • 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집
  • 코드가 실행되기 전임에도 불구하고 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있게 되는 셈이다.
  • 그렇다면 '자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다'라고 생각하더라도 코드를 해석하는 데는 문제될 것이 전혀없겠다.

hoisting

= 변수의 선언을 끌어올림

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

호이스팅이란 개념이 존재하지않는다면, (1)에는 함수 호출 시 전달한 1이 출력되고, (2)는 선언된 변수 x에 할당한 값이 없으므로 undefined가 출력되고, (3)에서는 2가 출력될것이다. 하지만 a의 인자로 1을 전달하지않고 x에 1을 바로 할당해보면?

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

arguments에 전달 된 인자를 담는 것을 제외하면 두번째 예제 코드의 내부에서 변수를 선언한 것과 다른 점이없다. 특히 LE입장에선 완전히 같다. 즉, 함수 내부의 다른 코드보다 먼저 선언 및 할당이 이뤄진 것으로 간주 할 수 있다.

 

✨ environmentRecord는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 어떤 값이 할당될 것인지는 관심이없다. == 변수를 호이스팅할때 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 남겨둔다. 그럼 위의 코드에서 호이스팅 된 상태를 보자.

function a() {
    var x;
    var x;
    var x;
    
    x = 1;
    console.log(x); // 1
    console.log(x); // 1
    x = 2;
    console.log(x); // 2
}
a(1);

이제 이코드가 우리가 예상한대로 1, undefined, 2가 출력되지않고 1, 1, 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함수가 출력될 것같다.

결과는 어떨까? 1️⃣ a함수를 실행하는 순간 a함수의 실행컨텍스트가 생성되고, 이때 2️⃣ 변수명과 함수 선언의 정보를 위로 끌어올린다(=수집한다 = LE에 담긴다(?)) 3️⃣ 변수는 선언부와 할당부를 나누어 선언부만 끌어올리는 반면 함수 선언은 전체를 끌어올린다.

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; 
    var b = function b() { } // <- 바뀐부분
    
    console.log(b); // (1)
    b = 'bbb';
    console.log(b); // (2)
    console.log(b); // (3)
}
a();

 

 

함수선언문과 함수표현식

함수를 새롭게 정의할 때 쓰이는 방식

함수선언문: function 정의부만 존재하고 별도의 변수에 할당하는 것

함수표현식: 함수를 다른 변수에 값으로써 '할당'한 것

// 함수 선언문: 함수명 a = 변수명
function a () {/*...*/}
a(); // 실행 ok

// (익명)함수 표현식: 변수명 b = 함수명
var b = function () {/*...*/}
b(); // 실행 ok

// 기명 함수 표현식: 변수명 c, 함수명 d
var c = function d () {/*...*/}
c(); // 실행 ok
d(); // 에러 // 기명함수는 외부에서 함수명으로 호출 불가 // 함수 내부에서 재귀함수를 호출하는 용도로 함수명 쓸 수 있음

함수선언문 vs 함수표현식

➡️ 함수 선언문은 전체를 호이스팅하는 반면 함수 표현식은 변수 선언부만 호이스팅한다.

console.log(sun(1,2));
console.log(multiply(3,4));

// 함수 선언문
function sum (a,b) {
	return a+b;
}

// 함수 표현식
var multiply = function (a,b) {
	return a*b;
}
// 함수 선언문은 전체를 호이스팅
var sum = function sum (a,b) {
	return a + b;
};

// 변수는 선언부만 호이스팅
var multiply;

console.log(sum(1,2));
console.log(multiply(3,4));
// multiply에는 값이 할당되어 
// 있지않은 상태에서 콘솔이 찍히므로,
// 에러 메세지 출력 됨

// 변수의 할당부는 원래 자리에
multiply = function (a,b) {
	return a*b;
};

🌻🤔 함수선언문 전체가 호이스팅되면서 변수sum에 할당된다(?)

 

✨ 함수 표현식으로 할당 된 multiply는 호이스팅이 제대로 되지않으면서 에러가 나지만 함수 선언문의 sum함수는 선언 전에 호출해도 아무 문제없이 실행된다. 오 그럼 함수 선언문이 제대로 호이스팅이 이루어지니까 더 좋은거아니야? 그렇지않다. 오히려 함수 선언문이 혼란을 가증시킬 수 있기때문에 사용이 자제된다.

함수 선언문이 혼란스러울 수 있는 이유?

  • 아래에서 선언한 것이 위에서 실행 됨
    • 동일한 변수명에 서로 다른 값을 할당할 경우 나중에 할당한 값이 먼저 할당한 값을 덮어씌움
    • 따라서 코드를 실행하는 중에 실제로 호출되는 함수는 오직 마지막에 할당한 함수, 즉 맨 마지막에 선언된 함수뿐이다.

⇒ 상대적으로 함수 표현식이 안전하다.

⇒ 원활한 협업을 위해서는 전역공간에 함수를 선언하거나 동명의 함수를 중복 선언하는 경우는 없어야한다.

⇒ 위의 경우가 만약 있더라도 모든 함수가 함수 표현식으로 정의돼 있었다면 에러가 발생하지 않을것이다.

스코프, 스코프 체인

스코프란?

  • 식별자에 대한 유효범위
  • 변수에 접근 할 수 있는 범위

global scope(=전역 스코프)
A의 외부에서 선언한 변수는 A의 외부뿐 아니라 A의 내부에서도 접근 가능

local scope(=지역 스코프)
A의 내부에서 선언한 변수는 오직 A의 내부에서만 접근 가능
예) 함수 스코프: 함수내에서 선연한 변수는 해당 함수 내에서만 접근 가능

Scope Chain(=스코프 체인)

안에서부터 바깥으로 차례로 검색해 나감

➡️ 지금까지 LE의 environmentRecord의 정보 수집 과정을 보았는데 드디어 LE 내부의 나머지 outerEnvironmentReference가 나온다.

- outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다.(??)

  •  스코프 체인 상에서 가장 먼저 발견된 식발자에만 접근 가능

 

스코프는 조금 더 자세하게 

>> 이 글에서 확인!

 

 

출처: 정재남, 코어 자바스크립트, 위키북스(2019), 2장 실행 컨텍스트