🔑 실행 컨텍스트는 식별자(변수, 함수, 클래스 드의 이름)를 등록하고 관리하는 스코프와 코드 실행 순서 관리를 구현한 내부 메커니즘으로, 모든 코드는 실행 컨텍스트를 통해 실행되고 관리됩니다.
실행 컨텍스트는 소스코드 평과와 실행을 통해 JavaScript의 로직을 실제로 구현하는 실체입니다.
전역 코드 평과: 전역 실행 컨텍스트 생성과 함께 전역 범위의 변수 선언문, 함수 선언문을 먼저 실행하여 실행 컨텍스트 환경(Global Lexical Enviroment)의 Key: Value
형태에서 key
에 식별자를 저장합니다. 이 과정에서 JavaScript의 Hoisting이 발생하게 되는 원인입니다.
함수 코드 평과: 함수 호출문을 만나가 되면 호출된 함수로 실행 순서가 변경되고 함수 실행 컨텍스트를 생성과 함께 매개변수, 지역 범위의 변수 선언문, 함수 선언문을 먼저 실행하여 실행 컨텍스트 환경(Function Lexical Enviroment)의 Key: Value
형태에서 key
에 식별자를 저장합니다. 또한 arguments 객체가 생성되어 지역 스코프에 등록되고 this 바인딩도 결정됩니다.
전역 코드 실행: 소스코드 실행 단계를 런타임이라고 지칭하며 선언문을 제외한 문들을 순차적으로 실행합니다. 이 과정에서 미리 선언문을 실행한 식별자의 존재유무를 확인하여 값을 할당하소 함수 호출문을 실행합니다.
함수 코드 실행: 전역 코드의 실행처럼 런타임이 시작되어 함수 코드가 순차적으로 실행됩니다. 이 과정에서 값의 할당과 내부 함수의 호출문을 실행합니다.
🔑 호이스팅은 JavaScript에서 선언문을 해당 스코프의 선두로 옮긴 것처럼 동작하는 특성입니다.
var 키워드와 함수 선언문은 스코프의 선두로 옮긴 것처럼 동작하여 선언문 이전에 식별자를 참조하게 되어도 Reference Error가 발생하지 않습니다.
// var 키워드
console.log(foo); // undefined
var foo;
// 함수 선언문
bar() // 'Hello World'
function bar() {
console.log('Hello World');
}
var 키워드는 호이스팅 현상이 발생하여 선언문 이전에 값을 참조하게 된다면 선언 단계와 초기화 단계가 한번에 진행되어 undefinde를 반환합니다.
함수 선언문으로 정의한 함수는 런타임 이전에 함수가 평가되어 함수 객체를 생성하고 자바스크립트 엔진이 함수 이름과 동일한 식별자를 암묵적으로 생성하여 생성된 함수 객체를 할당하는 특징으로 인해 함수 호출문을 함수 선언문보다 먼저 작성하여도 Reference Error가 아닌 함수 return 값을 반환합니다.
let, const 키워드로 선언된 변수는 선언 단계와 초기화 단계가 분리되어 진행됩니다. 이런 특징으로 선언문 이전에 값을 참조하게 된다면 Reference Errorr가 발생합니다. 그렇다고 let, const 키워드가 호이스팅이 발생하지 않은 것은 아닙니다.
// 선언과 초기화가 분리되어 진행됩니다.
console.log(foo); // ReferenceError: foo is not defined
let foo;
console.log(foo); // undefined
// 만약 호이스팅이 발생하지 않는다면 foo 값은 출력되어야 하지만 Reference Errorr가 발생합니다.
let foo = 1; // Global Scope
{
console.log(foo); // ReferenceError: foo is not defined
let foo = 2; // Local Scope
}
🔑 스코프는 식별자가 선언된 렉시컬 환경을 기억하여 참조할 식별자를 찾아내는 규칙입니다.
렉시컬 환경(Lexical Environment)은 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 참조를 기록하는 자료구조이며 내부적으로 두 부분으로 구성되어 있습니다.
Environment Record (환경 레코드): 지역 변수를 프로퍼티로 저장하고 있는 객체입니다. Environment Record는 소스코드의 타입에 따라 관리하는 내용이 다릅니다.
Outer Lexical Environment(외부 렉시컬 환경): 자신의 상위 렉시컬 환경을 참조합니다. Outer Lexical Environment를 통해 스코프 체인을 구현합니다.
var value = 1;
if (true) {
// if문 안에서만 x의 스코프를 지정하고 싶어도 var 키워드는 함수 레벨 스코프를 지원해서 if문의 스코프를 가질 수 없습니다.
var value = 2;
}
function functionLavelScope() {
// 함수에서 전역 변수 value와 동일한 변수를 선언했지만 함수 레벨 스코프로 인해 지역 변수를 가질 수 있습니다.
var value = 3;
console.log(value); // 3;
}
console.log(value); // 2;
let value = 1;
if (true) {
// if문도 블록문이므로 블록 레벨 스코프를 지원하여 if문만의 지역 스코프를 가질 수 있습니다.
let value = 2;
}
function functionLavelScope() {
// 함수에서 전역 변수 value와 동일한 변수를 선언했지만 함수 레벨 스코프로 인해 지역 변수를 가질 수 있습니다.
let value = 3;
console.log(value); // 3;
}
console.log(value); // 1;
이런 이유 외에 다양한 var 키워드의 문제가 있어서 var 키워드를 지양하고 let, const 키워드로 변수를 선언하는 습관을 가져야 합니다.
정적 스코프(렉스컬 스코프): 함수가 정의된 위치에 따라 상위 스코프를 결정합니다.
동적 스코프: 함수가 호출된 위치에 따라 상위 스코프를 결정합니다.
const x = 1;
function foo() {
const x = 2;
bar()
}
function bar() {
console.log(x); // 정적스코프, 동적 스코프에 따라 값이 달라진다.
}
만약 동적 스코프를 따르는 언어라면 x값은 2를 출력하게 됩니다. 하지만 JavaScrip는 정적 스코프(렉시컬 스코프) 방식에 따라 동작하여 전역 스코프에 정의된 bar 함수의 상위 스코프는 bar 함수가 어디서 호출되어도 동일한 상위 스코프를 유지합니다. 즉, JavaScript에서 x값은 1입니다.
let x = 1;
let y = 2;
function outerFunction(z) {
function innerFunction() {
let x = 10;
let w = 100;
console.log(x, y, z, w); // 10, 2 , 30, 100
}
innerFunction();
}
console.log(outerFunction(30));
Outer Lexical Environment(외부 렉시컬 환경)을 통해 상위 스코프에 접근할 수 있습니다. 스코프 체인은 단방향 링크드 리스트 자료구조를 사용하여 구현하여 역방향으로 스코프를 참조하는 문제를 방지했습니다. 또한 Outer Lexical Environment을 통해 상위 스코프를 찾아 Environment Record (환경 레코드)에 객체 형태로 저장된 식별자에 접근하는 방식이 스코프 체인입니다.
호이스팅은 소스코드 평과 단계에서 선언문을 먼저 실행하여 실행 단계에서 식별자 유무를 판단할 수 있게 하기 위해서 생겨나 JavaScript의 특수한 현상입니다. 호이스팅을 통해 식별자를 미리 실행 컨텍스트에 알려주면서 소스코드 실행 단계(런타임)에 스코프의 범위를 확인하여 식별자 참조를 하여 결과를 반환합니다. 이렇듯 호이스팅 과정을 통해 식별자 유무를 확인하고 스코프를 통해 식별자의 범위를 지정하는 관계를 가지고 있습니다.