JavaScript는 '{}' Block이 배배 꼬여 있어도 문법적으로는 잘 처리하지만, Block Scope은 지원하지 않는다. 그래서 JavaScript에서는 항상 함수 스코프를 사용한다.
function test() { // Scope
for(var i = 0; i < 10; i++) { // Scope이 아님
// count
}
console.log(i); // 10
}
Note: 할당할 때, 반환할 때, Function 인자에서 사용되는 것을 제외하면
{...}는 모두 객체 리터럴이 아니라 Block 구문으로 해석된다. 그래서 세미콜론을 자동으로 넣어주면 에러가 생길 수 있다.
그리고 JavaScript에는 Namepspace 개념이 없기 때문에 모든 값이 하나의 전역 스코프에 정의된다.
변수를 참조 할 때마다 JavaScript는 해당 변수를 찾을 때까지 상위 방향으로 스코프를 탐색한다. 변수 탐색하다가 전역 스코프에서도 찾지 못하면 ReferenceError를 발생시킨다.
// script A
foo = '42';
// script B
var foo = '42'
이 두 스크립트는 전혀 다르다. Script A는 전역 스코프에 foo라는 변수를 정의하는 것이고 Script B는 현 스코프에 변수 foo를 정의하는 것이다.
다시 말하지만, 이 둘은 전혀 다르고 var가 없을 때 특별한 의미가 있다.
// Global Scope
var foo = 42;
function test() {
// local Scope
foo = 21;
}
test();
foo; // 21
test 함수 안에 있는 'foo' 변수에 var 구문을 빼버리면 Global Scope의 foo의 값을 바꿔버린다. '뭐 이게 뭐가 문제야'라고 생각될 수 있지만 수천 줄인 JavaScript 코드에서 var를 빼먹어서 생긴 버그를 해결하는 것은 정말 어렵다.
// Global Scope
var items = [/* some list */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// Scope of subLoop
for(i = 0; i < 10; i++) { // var가 없다.
// 내가 for문도 해봐서 아는데...
}
}
subLoop 함수는 전역 변수 i의 값을 변경해버리기 때문에 외부에 있는 for문은 subLoop을 한번 호출하고 나면 종료된다. 두 번째 for문에 var를 사용하여 i를 정의하면 이 문제는 생기지 않는다. 즉, 의도적으로 외부 스코프의 변수를 사용하는 것이 아니라면 var를 꼭 넣어야 한다.
JavaScript에서 지역 변수는 함수의 파라미터와 var로 정의한 변수밖에 없다.
// 전역 공간 var foo = 1; var bar = 2; var i = 2;
function test(i) {
// test 함수의 지역 공간
i = 5;
var foo = 3;
bar = 4;
}
test(10);
foo 변수와 i 변수는 test함수 스코프에 있는 지역 변수라서 전역 공간에 있는 foo, i 값은 바뀌지 않는다. 하지만 bar는 전역 변수이기 때문에 전역 공간에 있는 bar의 값이 변경된다.
JavaScript는 선언문을 모두 **호이스트(Hoist)**한다. 호이스트란 var 구문이나 function 선언문을 해당 스코프의 맨 위로 옮기는 것을 말한다.
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
코드를 본격적으로 실행하기 전에 JavaScript는 var 구문과 function 선언문을 해당 스코프의 맨위로 옮긴다.
// var 구문이 여기로 옮겨짐.
var bar, someValue; // default to 'undefined'
// function 선언문도 여기로 옮겨짐
function test(data) {
var goo, i, e; // Block Scope은 없으므로 local 변수들은 여기로 옮겨짐
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // bar()가 아직 'undefined'이기 때문에 TypeError가 남
someValue = 42; // Hoisting은 할당문은 옮기지 않는다.
bar = function() {};
test();
블록 스코프(Block Scope)는 없으므로 for문과 if문 안에 있는 var 구문들까지도 모두 함수 스코프 앞쪽으로 옮겨진다. 그래서 if Block의 결과는 좀 이상해진다.
원래 코드에서 if Block은 전역 변수 goo를 바꾸는 것처럼 보였지만 호이스팅(Hoisting) 후에는 지역 변수를 바꾼다.
호이스팅을 모르면 다음과 같은 코드는 ReferenceError를 낼 것으로 생각할 것이다.
// SomeImportantThing이 초기화됐는지 검사한다.
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
var 구문은 전역 스코프의 맨위로 옮겨지기 때문에 이 코드는 잘 동작한다.
var SomeImportantThing;
// SomeImportantThing을 여기서 초기화하거나 말거나...
// SomeImportantThing는 선언돼 있다.
if (!SomeImportantThing) {
SomeImportantThing = {};
}
JavaScript의 모든 Scope은 현 객체를 가리키는 this를 가지고 있다. 전역 스코프에도 this가 있다.
함수 스코프에는 arguments라는 변수가 하나 더 있다. 이 변수는 함수에 인자로 넘겨진 값들이 담겨 있다.
예를 들어 함수 스코프에서 foo라는 변수에 접근할 때 JavaScript는 다음과 같은 순서로 찾는다.
- 해당 Scope에서
var foo구문으로 선언된 것을 찾는다. - Function 파라미터에서
foo라는 것을 찾는다. - 해당 Function 이름이
foo인지 찾는다. - 상위 Scope으로 있는지 확인하고 있으면 #1부터 다시 한다.
Note:
arguments라는 파라미터가 있으면 Function의 기본 객체인arguments가 생성되지 않는다.
JavaScript에서는 전역 공간(Namepspace) 하나밖에 없어서 변수 이름이 중복되기 쉽다. 하지만 *이름없는 랩퍼(Anonymous Wrappers)*를 통해 쉽게 피해갈 수 있다.
(function() {
// 일종의 네임스페이스라고 할 수 있다.
window.foo = function() {
// 이 클로저는 전역 스코프에 노출된다.
};
})(); // 함수를 정의하자마자 실행한다.
이름없는 함수는 표현식(expressions)이기 때문에 호출되려면 먼저 평가(Evaluate)돼야 한다.
( // 소괄호 안에 있는 것을 먼저 평가한다.
function() {}
) // 그리고 함수 객체를 반환한다.
() // 평가된 결과를 호출한다.
함수를 평가하고 바로 호출하는 방법이 몇가지 더 있다. 문법은 다르지만 똑같다.
// 함수를 평가하자마자 호출하는 방법들...
!function(){}();
+function(){}();
(function(){}());
// 등등...
코드를 캡슐화할 때는 항상 *이름없는 랩퍼(Anonymous Wrapper)*로 네임스페이스를 만들어 사용할 것을 추천한다. 이 래퍼(Wrapper)는 이름이 중복되는 것을 막아 주고 더 쉽게 모듈화할 수 있도록 해준다.
그리고 전역 변수를 사용하는 것은 좋지 못한 습관이다. 이유야 어쨌든 에러 나기 쉽고 관리하기도 어렵다.