자바스크립트의 호이스팅이란?
------------------------------------------------------------
유효영역(scope) 란?
자바스크립트에서 지역변수의 유효영역(scope)는 함수의 블록({}) 레벨에만 국한됩니다.
var x = 1;
console.log(x); 1
if (true) {
var x = 2;
console.log(x); //2
}
console.log(x); //2
이러한 이유로 자바스크립트의 유효영역은 함수 레벨 유효 영역(function-level scope)라고 합니다. 하지만 C언어의 경우 유효영역이 함수에만 국한되지 않습니다.
#include <stdio.h>
int main() {
int x = 1;
printf("%d", x); //1
if (1) {
int x = 2;
printf("%d", x); //2
}
printf("%d", x); //1
}
C언어는 자바스크립트와 다르게 if의 블록도 하나의 유효 영역이 됩니다. 이러한 이유로 C에서의 유효영역은 블록 레벨 유효 영역(block-level scope)라고 합니다.
------------------------------------------------------------
함수 선언문(function declaration)과 함수 표현식(function expression)
자바스크립트에서 함수 선언문과 함수 표현식은 다르다. 다음 코드는 함수 선언문입니다.
function foo() {}
하지만 이것 외에 모든 것은 함수 표현식입니다.
//익명 함수 표현식
var a = function () {
return 3;
};
//익명 즉시 실행 함수 표현식
(function() {})();
//기명 함수 표현식. 이것은 또한 클로저
var a = function bar() {
return 3;
}
//기명 즉시 실행 함수 표현식. 이것은 또한 클로저
(function bar() {
alert('hello!');
})();
이것의 차이는 분명한데 자바스크립트 인터프리터가 함수 선언문을 만나면 부모 함수 vo라는 key-value 공간(Scope공간)에 이 함수 객체를 참조하고 다음과 같은 일을 합니다.
function test( a, b ) {
var c, d;
c = 10;
foo();
//함수 선언문
function foo() {
}
//함수 표현식
d = function bar() {
};
d();
}
인터프리터가 함수의 정의를 해석하는 경우에는 다음과 같이 처리합니다. (완벽하지 않지만 흐름을 이해해주면 좋을 것 같습니다.)
//함수 해석시
test = new; //해쉬맵 생성
test.prototype = new Object();
test.vo = new VO();
test.vo.__parent__ = [부모 함수의 vo 참조. 여기선 window함수의 vo];
test.vo.c = undefined;
test.vo.d = undefined;
test.vo.a = 0;
test.vo.b = 1;
test.vo.foo = <foo>function;
test.arguments = null;
test.this = undefined;
test.vo.__parent__.test = test;
위 과정이 완벽하지 않지만 핵심은 test라는 해쉬맵 인스턴스를 생성하고 프로토타입과 vo 공간등을 만들고 마지막에 부모 함수의 vo에 test라는 키로 생성된 해쉬맵을 값으로 추가하도록 합니다. vo에는 var로 선언된 변수와 인자(arguments) 그리고 내부에 선언된 함수 선언문의 참조가 key-value로 잡히는 것도 확인하세요. 참고로 대신 부모함수인 window의 vo.__parent__는 null입니다. 그래서 이 사실이 프로토체인처럼 스코프 체인의 끝을 의미합니다.
또한 중요한 것은 test 함수 내부에 선언된 함수 선언문과 함수 표현식이 어떻게 해석되는지 눈여겨 봐주세요. 함수 선언문의 경우에는 해석시 바로 vo에 참조되지만 함수 표현식은 그렇지 않습니다. 즉 함수 표현식은 실행 때 해석하고 동작시킨다는 의미입니ㅏ.
실제로 test(3, 4)로 실행하게 되면 test.apply( null, [3, 4] ) 처럼 실행되고 실행 컨텍스트가 window가 되기 때문에 다음과 같은 과정을 거치게 됩니다.
//함수 실행시
EC = window.EC; //실행 컨텍스트(EC)
ECStack.push(EC);
EC = test.vo.clone(); //vo가 복사된다. 그래서 vo가 복잡하면 함수 실행할 때마다 성능에 영향을 줄 수 있다.
EC.arguments = {'0': 3, '1': 4, length:2};
EC.a = EC.arguments[0];
EC.b = EC.arguments[1];
EC.this = window;
(함수실행)
c = 10; //실행시에 vo에 있는 c를 찾아 10을 할당합니다. 만약 vo에 c가 없었다면 vo.__parent__, 즉 window 함수의 vo를 탐색할 것입니다. 이것이 바로 vo로 만들어지는 스코프 체인입니다. 스코프 체인을 통해 발견되지 않으면 객체 프로토타입 탐색이 이뤄집니다.
foo(); //함수 선언문이 실행됨. test함수의 vo객체에 key-value로 참조된 foo함수가 실행되는 것이다.
d = <bar>function;
d(); //함수 표현식이 실행됨. bar함수는 test함수가 실행될 때 해석되고 구동된다.
(함수실행끝)
ECStack.pop();
이렇듯 함수 선언문과 함수 표현식은 인터프리터에 의해 다른 시점에 해석되고 구동됩니다.
------------------------------------------------------------
var 선언
앞서 설명한 대로 함수가 인터프리터에 의해 해석되는 순간 하나의 해쉬맵 인스턴스가 만들어지고 VO 공간이 형성됩니다. 이 때 VO 공간에는 앞서 말했듯이 함수 선언문이 해석되어 참조됩니다. 그리고 또 한가지 참조되는 것은 바로 함수내에 var로 선언된 변수입니다. 함수 블록 레벨에 있는 var a, b; 로 쓰면 이 a, b는 VO 공간에 편입됩니다. 함수 내에서는 a와 b에 값을 할당하고 있지만 이 들 값들은 함수를 해석할 때가 아닌 실행할 때 할당됩니다.
------------------------------------------------------------
호이스팅이란?
함수의 호이스팅은 끌어올리기라고 많이 표현합니다. 즉 함수 내에 정의된 지역변수의 경우 함수 레벨 유효 영역의 어디에 위치하던지 항상 참조할 수 있는 변수가 됩니다. 즉 다음과 같은게 가능합니다.
a = 4;
function foo() {
a = 3;
console.log(a); //3
var a;
}
foo();
var a;가 함수 내에 어디에 위치하던지 함수가 선언이 되면 함수의 유효영역이 블록({}) 내부가 됩니다. 이 a는 위치에 상관없이 지역변수로써 쓸 수 있게되어 호이스팅이라는 말을 씁니다. 정말 지역변수를 끌어올리는 듯하죠.
자바스크립트의 호이스팅 대상은 2가지가 있습니다.
* 함수 선언문(function declaration)
* var 선언문
이것은 반드시 호이스팅 됩니다. 이들은 함수 블록의 중간에 정의 되더라고 항상 함수 머리에 정의된 것처럼 함수가 실행 됩니다. 호이스팅을 언급하기 전에 앞에서 함수 선언문과 var 선언된 변수는 모두 함수의 vo 공간에 함수가 해석될 시에 편입된다고 했습니다. 이제야 호이스팅의 진실을 알게 되었습니다. 결국 호이스팅이란 자바스크립트 인터프리터가 해석할 때 함수의 스코프 영역(vo)에 var로 선언한 변수와 함수 선언문의 참조를 key-value로 잡아주는 것 이상도 이하도 아닙니다. 호이스팅은 단지 끌어올린다는 개념은 너무 추상적인 것이고 불편한 진실인 셈이죠.
좀 더 살펴보지요.
function foo() {
bar();
var x = 1;
}
위 코드는 사실 아래처럼 해석됩니다.
function foo() {
var x;
bar();
x = 1;
}
다음 코드도 볼까요?
function foo() {
if( false ) {
var x = 1;
}
return;
var y = 1;
}
이것도 아래 처럼 해석됩니다.
function foo() {
var x, y;
if( false ) {
x = 1;
}
return;
y = 1;
}
함수의 경우는 어떨까요? 함수 선언문과 함수 표현식을 봅시다.
function test() {
foo(); //TypeError "foo 함수는 없다"
bar(); //"서!"
//함수 표현식
var foo = function() {
alert("달려!");
}
//함수 선언문
function bar() {
alert("서!");
}
}
test();
결국 함수 표현식은 var foo부분만 호이스팅 될 뿐이지 그 정의 자체는 함수 실행 타임에 해석됨을 의미한다.
------------------------------------------------------------
결론
내용이 좀 복잡했을지 모르겠습니다. 호이스팅을 설명하기 위해 언급한 내용들은 호이스팅을 구체적으로 이해시키는 것에 국한되지 않습니다. 그 외에 이론적 지식을 확장하는데 도움이 됩니다.
가령, 함수가 해석될 때 그 함수 내부에 구현되어 있는 함수 선언문이 많아지면 많아질수록 해석하는데 시간이 걸릴 것입니다. 그러므로 함수 선언문이 많으면 실행 초기에 성능 저하를 일으킬 수 있습니다. 그러므로 함수 선언문과 함수 표현식을 적절하게 섞어 잘 쓰셔야 하고요.
또 많은 책들이 [[Scope]] 체인 설명을 많이 하는데… 이것 또한 vo.__parent__ 라는 형태를 통해 체인을 형성한다는 구체적인 설명도 가능합니다.
간단하게 언급되었지만 함수 실행시 실행 컨텍스트(EC)가 운영되는 모습도 일종의 ECStack이라는 스텍 자료 구조에서 관리가 되는 것도 설명이 가능해집니다.
vo에 var 선언 및 인자(arguments)가 많아지만 성능에 저하를 일으킬 수 있다는 것도 알 수 있는데, 이는 함수 실행 때 마다 함수 해석때 만들어진 vo를 vo.clone()을 통해 매번 복사하기 때문입니다. 그래서 var선언 및 인자도 수를 제한시키는게 성능상 이득이 생깁니다.
사실상 자바스크립트에서의 유효 영역 레벨이 함수에 국한 되어 있다는 것도 함수를 해석할 때 var이 모두 vo에 잡힌다는 사실로 설명이 가능해집니다.
클로저가 생성된다는 말도 결국 부모-자식 함수에서 자식이 부모의 var로 선언된 것을 참조할 수 있는 영역이 발생한다는 말인데, 결국 vo의 연쇄가 바로 클로저인 겁니다. ^^
------------------------------------------------------------
참고