[JS] 실행 컨텍스트, 호이스팅, 스코프

Juno·2021년 2월 26일
7
post-thumbnail

✏️ 들어가기

  • 자바스크립트의 코드가 실행되는 과정
  • 실행 컨텍스트
  • 호이스팅
  • 스코프(스코프 체인)

실행 컨텍스트

실행 컨텍스트란, 실행할 코드에 제공할 환경 정보들을 모아놓은 객체 입니다.

일반적으로, 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것 뿐입니다

프로그램의 실행은, 실행할 코드에 제공할 환경 정보들을 모아 실행 컨텍스트를 만들고,

이를 콜 스택에 쌓아올렸다가 콜스택 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장합니다!

콜 스택

콜 스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점입니다

// -------------------------(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

(그림 2-3) 참고 / (값의 출력에 대한 부분은 뒷 부분에서 설명)

  1. (1) 전역 컨텍스트가 콜 스택에 담김(자바스크립트의 파일이 실행되는 순간 담긴다)
  2. (3) outer() 함수를 호출하면서 outer 실행 컨텍스트 생성 후 콜 스택에 담김(콜 스택에 담긴 최상단이 outer 이므로 outer의 코드를 실행한다.)
  3. (2) 에서 inner() 를 호출하면서 outer의 실행을 잠시 중단하고 inner() 를 콜 스택에 담는다.
  4. inner() 내부에서 a = 3 할당 후 inner 함수를 콜 스택에서 제거(pop)
  5. outer() 에서 a 출력 후 outer 함수를 콜 스택에서 제거(pop)
  6. 전역 컨텍스트에서 a 출력 후 전역 컨텍스트도 콜 스택에서 제거 후 종료

자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는 데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장한다

이때, 실행 컨텍스트가 수집하는 세 가지 정보는 다음과 같습니다~~

한글 직역은 오히려 이해를 해친다고 하여, 좀 어려워도 영어 그대로 느낌만 가져갈게요 😁

  1. VariableEnvironment : 현재 컨텍스트 내의 식별자들에 대한 정보 담긴 객체(변경 사항은 반영되지 않음, 최초 실행 시 의 스냅샷을 유지함)
    • environmentRecord : 매개변수명, 변수의 식별자, 선언한 함수의 함수명 등을 수집
    • outerEnvironmentReference : 바로 직전 컨텍스트의 LexicalEnvironment 정보 참조
  2. LexicalEnvironment : 변경 사항이 실시간으로 반영되는 정보 담긴 객체 - 스코프 탐색을 위해 사용된다.
    • environmentRecord
    • outerEnvironmentReference
  3. ThisBinding : this 식별자가 바라보는 대상 객체(3장에서 자세히)

식별자란?
컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언한 함수가 있을 경우 함수 자체, 선언된 변수의 식별자 등이 식별자에 해당합니다!


EnvironmentRecord와 호이스팅

environmentRecord 에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됩니다

실행 컨텍스트가 실행되기 이전에, 컨텍스트 내부 전체를 처음부터 끝까지 순서대로 식별자들을 수집하는 이 과정을 호이스팅 이라고 합니다.

따라서 코드가 실행되기 이전임에도 불구하고 자바스크립트 엔진은 해당 환경에 속한 코드의 변수명들을 모두 알고있게 됩니다.

호이스팅

자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행

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), (2), (3)의 출력 결과를 각각 예측해 보세요~!

(설명의 편의상 a(1) 을 var x = 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;          // 수집 대상 3의 할당 부
	console.log(x); // (3)
}
a(1);
  1. 첫번재 줄 부터 순서대로 var x를 선언하여 메모리에 저장할 공간을 미리 확보하고, 주솟값을 변수 x에 연결합니다.
  2. 두번째, 세번째 줄에선 다시 x를 선언합니다 (이미 선언되어 있으므로 무시)
  3. x = 1 을 대입합니다(1을 데이터 영역의 메모리에 담고 x의 값에 1의 주솟값을 연결)
  4. (1)과 (2) 모두 1이 출력됩니다
  5. x = 2 을 대입합니다.(2는 데이터 영역에 존재하지 않으므로 새로 담고, x의 값에 2의 주솟값을 연결)
  6. (3)은 2가 출력됩니다.
  7. 실행 컨텍스트가 콜 스택에서 모두 제거되며 종료됩니다.

호이스팅 개념을 고려하지 않은 경우와 고려한 경우의 결과를 같게 예상하셨나요??

변수 뿐만 아니라, 함수를 선언한 경우도 살펴보겠습니다 😁

함수 선언의 호이스팅

function a() {
	console.log(b); // (1)
	var b = 'bbb';  // 수집 대상 1(변수 선언)
	console.log(b); // (2)
	function b(){}  // 수집 대상 2(함수 선언)
	console.log(b); // (3)
}
a();

❓(1), (2), (3)의 출력 결과를 각각 예측해 보세요~!

함수 선언의 호이스팅 이후

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

이 코드에서 function b(){}는 호이스팅이 끝나면, var b = function b(){}와 같이 여길 수 있습니다

  1. 첫째줄에서 var b를 선언하여 메모리에서 b를 저장할 공간을 마련하고, 주솟값을 연결해둡니다(1003 등)
  2. 둘째줄에서 function b의 주솟값을 변수 b의 값에 넣어 연결합니다(할당)
  3. (1)은 함수b가 출력됩니다
  4. 'bbb'라는 값을 담은 데이터를 만들고, 'bbb'의 주솟값을 b의 값으로 새로 연결합니다
  5. (2),(3) 모두 'bbb'가 출력됩니다

함수 선언문과 함수 표현식

function a(){} // 함수 선언문
a();

var b = function(){} // (익명)함수 표현식 
b();
  • 함수 선언문 : function 정의부만 존재하고 별도의 할당 명령이 없는 것
  • 함수 표현식 : 정의한 function을 별도의 변수에 할당하는 것

둘 다 함수를 새롭게 정의할 때 쓰는 방식이지만, 호이스팅 개념 때문에 두 방식에서 차이가 발생할 수 있습니다

다음의 예시를 보고 호이스팅까지 적용한 코드도 보겠습니다

// 호이스팅 적용 전
console.log(sum(1,2));      // (1)
console.log(multiply(3,4)); // (2)

function sum(a,b){
	return a + b;
}

var multiply = function(a,b){
	 return a * b;
}

❓(1), (2) 의 출력 결과를 각각 예측해 보세요~!

// 호이스팅 적용 후
var sum = function sum(a,b){
	return a + b;
}
var multiply;

console.log(sum(1,2));      // 3
console.log(multiply(3,4)); // undefined 

multiply = function(a,b){
	return a * b
} 

함수 선언문은 전체를 호이스팅한 반면, 함수 표현식은 변수 선언부만 호이스팅 했습니다.

함수 표현식은 함수를 다른 변수에 "값"으로써 할당한 것 입니다!

함수 선언문의 방식으로 함수를 정의했을 경우, 코드에서의 순서와 상관없이 어느곳에서도 실행이 가능합니다.

하지만, 함수 표현식의 경우엔 함수가 정의된 이후에만 실행될 수 있습니다.

그 이유로 함수 선언문으로 동일한 이름의 함수를 여러개 작성하면, 맨 마지막에 선언된 함수로 덮여 씌어집니다.

따라서, 상대적으로 안전한 함수 표현식을 사용하는 것을 권장합니다.

(p52-p53 함수 선언문의 위험성 에서 예시 참고)

스코프, 스코프 체인

스코프란 식별자에 대한 유효범위 를 말하고, 흔히들 알고 있는 지역변수와 전역변수의 개념입니다

자바스크립트의 스코프는 오직 함수에 의해서만 생성됩니다.

스코프 체인이란 스코프 체인식별자의 유효범위를 안에서 부터 바깥으로 차례로 검색해나가는 것을 스코프 체인 이라고 합니다. 이때, 식별자의 유효범위를 검색할 수 있도록 정보를 수집해온 것이 바로 LexicalEnvironment의 두번째 수집 자료인 outerEnvironmentReference 입니다.

outerEnvironmentReference 는 현재 호출된 함수가 선언될 당시(과거)의 LexicalEvironment를 참조합니다.

아이패드 설명 참고

오직 자신이 선언된 시점의 LexicalEnv만 참조하고 있으므로 가장 가까운 요소부터 차례대로 접근할 수 있습니다

스코프 상에서 가장 먼저 발견된 식별자에만 접근 가능!

콜 스택에서 나왔던 예시 코드(스코프 체인 설명)

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

전역 컨택스트

  • record : { a, outer }
  • reference : 없음(선언 시점이 없으므로)

outer

  • record : { inner }
  • reference : [ 전역객체,{ a, outer } ]

inner

  • record : { a }
  • reference : [ outer , { inner } ]

  1. (1) 에서 a의 값을 출력하기 위해 현재 활성화 상태인 inner()의 LexicalEnvironment에서 a를 검색합니다. a는 존재하지만 값이 할당 되지 않았으므로 undefined를 출력합니다.
  2. (2) 에서 a의 값을 출력하기 위해 현재 활성화 상태인 outer() 의 LexiccalEnvironment에서 a를 검색합니다. record에서 찾을 수 없으므로, reference에 있는 record로 넘어가서(스코프 체인, 안에서 바깥으로) a가 있는지를 검사합니다. 이때 전역객체의 record에 a가 있고 그 값이 1이므로 1을 출력합니다.
  3. (3) 에서 a의 값을 출력하기 위해 전역객체 의 LexicalEnvironment에서 a를 검색합니다. record에 a가 존재하고 1이라는 값이 할당되어 있으므로 1을 출력하고 종료합니다.

스코프 체인(안→밖) : inner 함수 → outer 함수 → 전역 컨텍스트

스코프 체인 : 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것

  • inner함수 : inner, outer, 전역 스코프 모두 접근 가능
  • outer함수 : outer, 전역 스코프 접근 가능
  • 전역 컨택스트 : 현재

위 설명의 1번 에서 inner() 의 a는 inner에서도 전역에서도 선언했습니다.

하지만, inner함수 내부에서 a에 접근하려고 하면 무조건 스코프 체인 상의 첫 번째 인자,

즉 inner 스코프의 lexicalEnvironment부터 검색해야 합니다.

이때 inner 스코프 내에 a가 있으므로 더이상 진행하지 않고 a를 즉시 반환합니다

📝 정리

실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체입니다

실행 컨텍스트 객체는 활성화되는 시점에 VariableEnvironment, LexicalEnvironment, Thisbinding의 세 가지 정보를 수집합니다

실행 컨텍스트는 VariableEnvironmentLexicalEnvironment가 동일한 내용으로 구성되지만, LexicalEnvironment는 함수 실행 도중에 변경사항이 즉시 반영되는 반면, VariableEnvironment는 초기상태를 유지합니다.

LexicalEnvironment 는 매개변수명, 변수의 식별자, 선언한 함수의 함수명 등을 수집하는 environmentRecord와 바로 직전 컨텍스트의 LexicalEnvironment 정보를 참조하는 outerEnvironmentReference로 구성돼 있습니다.

호이스팅은 코드 해석을 좀 더 수월하게 하기 위해 environment의 수집 과정을 추상화한 개념으로, 실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 끌어올린다고 해석하는 것입니다. 변수 선언과 값 할당이 동시에 이뤄진 문장은 선언부 만을 호이스팅 합니다.(할당 과정은 남아있음)

스코프는 변수의 유효범위를 말합니다. outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironment를 참조합니다. 코드 상에서 어떤 변수에 접근하려고 하면 현재 컨텍스트의 LexicalEnvironment를 탐색해서 발견되면 그 값을 반환하고, 발견하지 못할 경우 다시 outerEnvironmentRecord에 담긴 LexicalEnvironment를 탐색하는 과정을 거칩니다. 전역 컨텍스트의 LexicalEnvironment까지 탐색해도 해당 변수를 찾지 못하면 undefined를 반환합니다.

profile
사실은 내가 보려고 기록한 것 😆

0개의 댓글