여기서부터는 활성 객체와 변수 객체를 변수 객체라는 표현으로 통일한다
이 절에서는 5.2 실행 컨텍스트 생성 과정에서 설명한 스코프 체인이 어떻게 만들어지는지 살펴본다
자바스크립트 코드를 이해하려면 스코프 체인의 이해는 필수적이다
이를 알아야 변수에 대한 인식 메커니즘을 알 수 있고, 현재 사용되는 변수가 어디에서 선언된 변수인지 정확히 알 수 있기 때문이다
자바스크립트도 다른 언어와 마찬가지로 스코프, 즉 유효 범위가 있다
이 유효 범위 안에서 변수와 함수가 존재한다
다음은 C 언어의 유효 범위를 확인할 수 있는 코드이다
void example_scope() {
int i = 0;
int value = 1;
for(i = 0; i < 10; i++) {
int a = 10;
}
printf("a : $d", a); // 컴파일 에러
if (i == 10) {
int b = 20;
}
printf("b : %d", b); // 컴파일 에러
printf("value %d", value); // 1
}
C 언어에선 중괄호로 묶여 있는 범위 안에서 선언된 변수는 블록이 끝나는 순간 사라진다
특히, 함수의 {}뿐만 아니라 if, for 문의 {}도 한 블록으로 묶여, 그 안에서 선언된 변수가 밖에서는 접근이 불가능하다
하지만 자바스크립트에서는 함수 내의 {}블록은, 이를 테면 if{}, for{}와 같은 구문은 유효 범위가 없다
자바스크립트는 오직 함수만이 유효 범위의 한 단위가 된다
이 유효 범위를 나타내는 스코프가 [[scope]] 프로퍼티로 각 함수 객체 내에서 연결 리스트 형식으로 관리되는데, 이를 스코프 체인이라 한다
스코프 체인은 각 실행 컨텍스트의 변수 객체가 구성 요소인 리스트이다
각각의 함수는 [[scope]] 프로퍼티로 자신이 생성된 실행 컨텍스트의 스코프 체인을 참조한다
함수가 실행되는 순간 실행 컨텍스트가 만들어지고, 이 실행 컨텍스트는 실행된 함수의 [[scope]] 프로퍼티를 기반으로 새로운 스코프 체인을 만든다
다음의 규칙을 기억하자
var var1 = 1;
var var2 = 2;
console.log(var1);
console.log(var2);
위 예제는 전역 코드이며 함수가 선언되지 않아 함수 호출이 없고, 실행 가능한 코드들만 나열되어 있다
이 자바스크립트 코드를 실행하면, 먼저 전역 실행 컨텍스트가 생성되고, 변수 객체가 만들어진다
이 변수 객체의 스코프 체인은 현재 전역 실행 컨텍스트 단 하나만 실행 되고 있어 참조할 상위 컨텍스트가 없으므로 자기 자신만을 가진다
즉, 변수 객체의 [[scope]] 프로퍼티는 변수 객체 자신을 가리키고 있다
그 후 var1, var2와 같은 변수들이 생성되고 이들은 변수 객체에 의해 참조된다
이 변수 객체는 전역 컨텍스트의 변수 객체이므로 이 변수 객체가 곧 전역 객체가 된다
var var1 = 1;
var var2 = 2;
function func() {
var var1 = 10;
var var2 = 20;
console.log(var1); // 10
console.log(var2); // 20
}
func();
console.log(var1); // 1
console.log(var2); // 2
이 예제를 실행하면 전역 실행 컨텍스트 생성되고, func() 함수 객체가 생성된다
이 함수 객체의 [[scope]]는 어떻게 될까?
위에서 설명한 규칙대로, 자신이 생성된 실행 컨텍스트의 스코프 체인을 참조한다
즉, 함수가 생성될 때 그 함수 객체의 [[scope]]는 현재 실행되는 컨텍스트의 변수 객체에 있는 [[scope]]를 그대로 가진다
따라서 func 함수 객체의 [[scope]]는 전역 변수 객체가 된다
이후, 함수를 실행하면 새로운 컨텍스트가 만들어진다
새로 생성된 func 컨텍스트의 스코프 체인은 실행된 함수의 [[scope]] 프로퍼티를 그대로 복사한 후, 현재 생성된 변수 객체를 스코프 체인의 맨 앞에 추가한다
func 함수 객체가 [[scope]]로 전역 변수 객체 하나만을 가지고 있었으므로, 여기에 func 변수 객체를 추가한다
즉, func 실행 컨텍스트의 스코프 체인은 [func 변수 객체 - 전역 객체]의 리스트가 된다
[[scope]]
생성된 함수 객체의 [[scope]]는 자신을 생성한 컨텍스트의 스코프 체인을 참조한다
함수의 이 [[scope]] 프로퍼티는 차후 함수 실행으로 생성된 새로운 컨텍스트의 스코프 체인에 의해 참조된다
새로운 컨텍스트의 스코프 체인은 이 함수의 [[scope]] 프로퍼티의 복사본에 자신의 변수 객체를 그 앞에 추가하는 방법으로 만들어진다
스코프 체인
각 함수 객체는 [[scope]] 프로퍼티로 현재 컨텍스트의 스코프 체인을 참조한다
한 함수가 실행되면 새로운 실행 컨텍스트가 만들어지는데, 이 새로운 실행 컨텍스트는 현재 실행 되는 함수 객체의 [[scope]] 프로퍼티를 그대로 복사한 뒤 그 체인의 제일 앞에 새롭게 생성된 변수 객체를 추가하는 방법으로 자신의 스코프 체인을 만든다
요약하면 스코프 체인 = 현재 실행 컨텍스트의 변수 객체 + 상위 컨텍스트의 스코프 체인 이라고 할 수 있다
아래 두 예제의 차이점을 명확이 알아두자
예제 1
var value = "value1";
function printFunc() {
var value = "value2";
function printValue() {
return value;
}
console.log(printValue()); // value2
}
printFunc();
예제 2
var value = "value1";
function printValue() {
return value;
}
function printFunc(func) {
var value = "value2";
console.log(func());
}
printFunc(printValue); // value1
이렇게 만들어진 스코프 체인으로 식별자 인식이 이루어진다
식별자 인식은 스코프 체인의 첫 번째 변수 객체부터 시작하여, 식별자와 대응되는 이름을 가진 프로퍼티가 있으면 이를 참조하고, 없으면 다음 변수 객체로 이동하여 찾는다
여기서 this는 식별자가 아닌 키워드로 분류되므로, 스코프 체인의 참조 없이 접근할 수 있다
호이스팅
여기까지 이해했으면 4.1.5 함수 호이스팅의 원리를 이해해야 한다
foo();
bar();
var foo = function () {
console.log("This is foo, x = " + x);
};
function bar () {
console.log("This is bar, x = " + x);
};
var x = 1;
위 코드는 다음과 같은 순서로 진행된다
// 변수 생성
var foo;
function bar () {
console.log("This is bar, x = " + x);
};
var x;
// 코드 실행
foo(); // TypeError
bar(); // This is bar, x = undefined
foo = function () {
console.log("This is foo, x = " + x);
};
x = 1;
함수 생성 과정에서 변수 foo, 객체 bar, 변수 x가 차례대로 생성된다
이때 foo와 x에는 undefined가 할당된다
코드가 실행되면 foo(); 에서 TypeError가 발생한다
이는 foo가 아직 함수가 아니고 undefined이기 때문이다
bar();를 실행하면 bar 실행 컨텍스트가 생성되고 내부 코드가 실행되나, x는 아직 undefined이므로 This is bar, x = undefined가 출력된다
x에 1이 할당된다
출처: 송형주, 고현준 저, 『인사이드 자바스크립트』, 한빛미디어(2014)