본문 바로가기

자스핑

[week 02] 실행 컨텍스트와 스코프, 호이스팅


 

🪄 실행 컨텍스트

코드가 실행되기 위해 필요한 환경 정보들을 모아놓은 객체를 말합니다.
실행 컨택스트의 동작은, 동일한 환경에 있는 코드들을 실행할 때 필요한 환경정보를 모아 컨텍스트를 구성하고, 이를 콜 스택에 쌓아놓은 뒤, 가장 위에 있는 컨텍스트와 관련 있는 코드들을 실행하는 것으로 코드의 환경과 순서를 보장합니다.
 
자바스크립트에서 하나의 실행 컨텍스트를 구성할 수 있는 방법에 대해 알아볼까요?
 
1️⃣ 전역 코드 : 전역 영역에 존재하는 코드
2️⃣ Eval 코드 : eval 함수로 실행되는 코드
3️⃣ 함수 코드 : 함수 내에 존재하는 코드
4️⃣ (ES6부터는) 블록문
 
가장 쉽게 자바스크립트에서 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것입니다! 어떤 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장합니다. 실행 컨텍스트는 논리적 스택 구조를 가지는데 실행되는 순서대로 콜 스택(call stack)에 쌓였다가, 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 동일한 환경과 순서를 보장합니다.
 


 
 

🪄 호이스팅 (hoisting)

자바스크립트 엔진은 소스코드를 한 줄씩 순차적으로 실행하기에 앞서 먼저 소스코드의 평가 과정을 거치며 실행하기 위한 준비를 합니다. 그 과정에서 자바스크립트 엔진은 변수 선언을 포함한 모든 선언문 (변수, 함수 등)을 소스코드에서 찾아 제일 먼저 실행합니다. 이렇게 선언문이 런타임 이전 단계에서 제일 먼저, 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트의 고유 특징을 호이스팅이라 합니다!
 
 

1️⃣ 변수 호이스팅 (var vs let)

var 키워드의 경우,
런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 "선언 단계"와 "초기화 단계"가 한번에 진행된다.
(1) 선언 단계에서 스코프에 변수 식별자를 등록해 자바스크립트 엔진에 변수의 존재를 알린다.
(2) 그리고 즉시 초기화 단계에서 undefined로 변수를 초기화한다.
(3) 따라서 변수 선언문 이전에 변수에 접근해도 스코프에 변수가 존재하기 때문에 에러 발생 x
(4) 변수 할당문에 도달하면 값 할당
 

//호이스팅으로 인한 선언 단계
console.log(foo); //undefined

var foo;
console.log(foo); //undefined

//할당 단계
foo = 1; 
console.log(foo); //1

 
let 키워드의 경우,
선언한 변수는 "선언 단계"와 "초기화 단계"가 분리되어 진행된다.
(1) 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 선언 단계가 실행되지만, 
(2) 초기화 단계는 변수 선언문에 도달했을 때 실행된다. -> ReferenceError
 
🪄 일시적 사각지대 Temporal Dead Zone
let 키워드로 선언한 변수는 스코프의 시작 지점부터 초기화 단계 시작 지점(변수 선언문)까지 변수를 참조할 수 없다.
스코프의 시작 지점부터 초기화 시작 지점까지 변수를 참조할 수 없는 구간을 말한다.

//런타임 이전에 선언 단계가 실행
//변수 초기화는 되지 않음

console.log(foo); //ReferenceError: foo is not defined.

let foo; //선언문에서 초기화 단계가 실행된다
console.log(foo); //undefinde

foo = 1; //할당문에서 할당 단계가 실행된다.
console.log(foo); //1

 
 
let은 변수 호이스팅이 발생하지 않는 것처럼 보이지만,

let foo = 1; //전역 변수
{
	console.log(foo); //ReferenceError: cannot access 'foo' before initialization
    let foo = 2; //지역 변수
}

 
호이스팅이 발생하지 않는다면, 전역 변수 foo 값을 출력해야 한다.
하지만 여전히 호이스팅이 발생하기 때문에 참조 에러가 발생한다.

 
 
 

2️⃣ 함수 호이스팅

함수 선언문으로 정의한 함수는 함수 선언문 이전에 호출이 가능하다.
함수 표현식으로 정의한 함수는 함수 표현식 이전에 호출할 수 없다.
함수 선언문으로 정의한 함수와 함수 표현식으로 정의한 함수의 생성 시점이 다르다.
 
🪄  함수 선언문

//함수 선언문
function add (x, y) {
	return x + y;
}

//함수 참조
console.dir(add); // f add(x, y)

//함수 호출
console.log(add(2,5)); // 7

console.dir은 console.log와 달리 함수 객체의 프로퍼티까지 출력한다.

 
함수 선언문은 표현식이 아닌 문이다.
따라서, 콘솔에서 함수 선언문을 실행하면 완료값 undefined가 출력된다.
표현식이 아닌 문은 변수에 할당할 수 없다.
 
 
🪄 함수 표현식
자바스크립트의 함수는 객체 타입의 값이다.
따라서 함수는 값처럼 변수에 할당할 수도 있고, 프로퍼티 값이 될 수도 있으며, 배열의 요소가 될 수도 있다.
이처럼 값의 성질을 갖는 객체를 일급 객체라 하며, 자바스크립트 함수는 일급 객체이다.
자바스크립트 함수를 값처럼 자유롭게 사용할 수 있다는 의미다.
 

//함수 표현식
var add = function(x, y) {
	return x + y;
};

console.log(add(2, 5)); // 7

함수 리터럴의 함수 이름은 생략 가능하며, 이를 익명함수라 한다.

 
함수는 일급 객체이므로 함수 리터럴로 생성한 함수 객체를 변수에 할당할 수 있다.
이런 함수 정의 방식을 함수 표현식이라 한다.
함수 표현식은 표현식인 문이다.
 
 
 

🪄 함수 선언문 호이스팅 ( = 함수 호이스팅)

(1) 런타임 이전에 js엔진에 의해 먼저 실행
(2) 함수 객체가 먼저 생성
(3) 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 생성된 함수 객체를 할당
(4) 런타임엔 이미 함수 객체가 생성되어 있고, 함수 이름과 동일한 식별자에 할당까지 완료된 상태.
=> 함수 선언문 이전에 함수를 참조할 수 있으며, 호출이 가능하다. ( = 호이스팅 )
 
 

🪄  함수 표현식 호이스팅 ( = 변수 호이스팅)

변수에 할당되는 값이 함수 리터럴인 문
(1) 변수 선언은 런타임 이전에 실행되어, undefined로 초기화 되지만, ( → 타입에러 )
(2) 변수 할당문의 값은 할당문이 실행되는 시점 = 런타임에 평가되므로
(3) 함수 표현식의 함수 리터럴도 할당문이 실행되는 시점에 평가되어 함수 객체가 됨.
따라서, 함수 표현식으로 정의를 하면, 함수 호이스팅이 아니라 변수 호이스팅이 발생한다.
 
 

⚡️ 함수 호이스팅 vs 변수 호이스팅

- 공통점
런타임 이전에 js엔진에 의해 먼저 실행되어 식별자를 생성한다는 점에서 동일하다.
 
- 변수 호이스팅 (var 키워드를 사용한 변수 선언문)
undefined로 초기화된다. 변수 선언문 이전에 변수를 참조하면 변수 호이스팅에 의해 undefined로 평가된다
 
- 함수 호이스팅 (함수 선언문)
암묵적으로 생성된 식별자는 함수 객체로 초기화되어 함수 선언문 이전에 호출하면 함수 호이스팅에 의해 호출된다.
 
 

** 함수 호이스팅은 함수 호출 전 반드시 함수를 선언해야 한다는 당연한 규칙을 무시하기 때문에,
함수 선언문 대신 함수 표현식을 사용할 것을 권장한다.

 
 


 
 

📍 스코프

스코프란, 식별자가 유효한 범위를 말하며, 식별자를 검색할 때 사용하는 규칙이라고도 할 수 있다.
 
스코프는 적용 범위에 따라 블록 레벨 스코프, 함수 레벨 스코프로 나누어 볼 수 있는데,
✅ 블록 레벨 스코프를 적용할 경우, 아래 코드에서 if문 블록 내에서 선언된 변수는 if문 내에서 지역변수로 사용되고,
✅ 함수 레벨 스코프를 적용할 경우, 아래 코드에서 if문 블록 내에서 선언된 변수는 전역 변수로 사용된다.
 

var var1 = 1; // 코드의 가장 바깥 영역에서 선언한 변수

if (true) {
	var var2 = 2; // 코드 블록 내에서 선언한 변수
    if (true) {
    var var3 = 3; // 중첩된 코드 블록 내에서 선언한 변수
    }
}

function foo() {
	var var4 = 4; // 함수 내에서 선언한 변수
    
    function bar() {
    var var5 = 5; // 중첩된 함수 내에서 선언한 변수
  	}
}

 
코드에서는 var로 변수를 선언했기 때문에, 함수 레벨 스코프를 적용하게 된다.
(var : 함수 레벨 스코프 | let, const : 블록 레벨 스코프)
 

🤔 var 변수를 선언하지 않는 이유 하나가 바로 이것!
함수 레벨 스코프를 적용하게 되면, if 블록 내에서 선언된 변수도 전역변수가 되는데,
"전역변수" 선언하는 것을 지양하는 것이 좋으므로, 전역변수를 많이 만들어버리는 var 쓰는 것이다!