JavaScript · 6장 함수 심화
오래된 var, 전역 객체, 즉시 실행 함수
새 코드는 let과 const만 써요(변수). 그래도 var를 알아야 하는 이유가 있어요. 오래된 코드 · 라이브러리 · 검색해서 찾은 예제에 아직 많이 남아 있고, 그 코드의 버그는 대개 var의 성질에서 와요. 지금도 쓰이는 즉시 실행 함수와 globalThis까지 함께 짧게 정리해요.
01. var는 블록을 무시해요
let · const는 중괄호 블록마다 상자를 만들지만, var는 함수 단위로만 상자를 만들어요. if · for 블록 안에서 선언해도 함수 전체에서 보이고, 같은 이름으로 다시 선언해도 오류가 나지 않아요.
function check() {
if (true) {
var inside = '블록 안';
}
return inside; // let이었다면 ReferenceError
}
console.log(check()); // 블록 안
var count = 1;
var count = 2; // 두 번 선언해도 조용히 덮어써요
console.log(count); // 2
for (var i = 0; i < 3; i++) {}
console.log(i); // 3 — 반복문 변수가 밖으로 새어 나와요
이 성질이 클로저(클로저)와 만나면 유명한 버그가 생겨요. 반복문 안에서 만든 함수들이 상자 하나에 든 var i를 함께 가리켜서, 나중에 부르면 모두 반복이 끝난 뒤의 값을 봐요. let은 반복할 때마다 새 상자를 만들어서 각 함수가 그 회차의 값을 기억해요.
var handlers = [];
for (var i = 1; i <= 3; i++) {
handlers.push(() => i + '번 탭 열기');
}
console.log(handlers.map(h => h())); // ['4번 탭 열기', '4번 탭 열기', '4번 탭 열기']
const fixed = [];
for (let j = 1; j <= 3; j++) {
fixed.push(() => j + '번 탭 열기');
}
console.log(fixed.map(h => h())); // ['1번 탭 열기', '2번 탭 열기', '3번 탭 열기']
02. 선언이 위로 끌어올려져요
자바스크립트는 코드를 실행하기 전에 상자 안의 선언을 먼저 훑어요. 이때 var 변수는 undefined로 미리 만들어 두고, 함수 선언문은 함수 전체를 미리 만들어 둬요. 그래서 선언보다 위에서 써도 오류가 나지 않아요. 이를 끌어올리기(호이스팅)라고 해요. let · const도 미리 자리는 잡지만, 선언 줄에 닿기 전에 쓰면 오류를 내 줘요(선언 전에 쓰기).
console.log(price); // undefined — 오류 대신 빈 값이 조용히 나와요
var price = 5900;
console.log(greet()); // 안녕 — 함수 선언문은 통째로 끌어올려져요
function greet() { return '안녕'; }
console.log(total); // let은?
let total = 0;
var의 undefined는 "아직 값을 넣기 전에 읽었다"는 실수를 숨겨요. let · const가 오류를 내는 쪽이 버그를 빨리 찾게 해 줘요. 함수 선언문이 끌어올려지는 성질은 지금도 써요. 파일 위쪽에 큰 흐름을, 아래쪽에 도우미 함수를 두는 식으로요.
03. 전역 객체와 globalThis
어디서나 보이는 전역 변수와 함수(setTimeout, fetch, JSON…)는 전역 객체의 속성이에요. 브라우저 화면에서는 window, Web Worker에서는 self, Node.js에서는 global로 이름이 달라서, 어디서나 같은 이름으로 쓰라고 만든 게 globalThis예요. 이 글의 실행 블록은 Web Worker에서 돌아서 window가 없는데, globalThis는 있어요.
console.log(typeof window, typeof globalThis); // undefined object — 실행 블록은 Worker라서
var appName = 'SIMPLE'; // 최상위 var는 전역 객체의 속성이 돼요
let version = 2; // let · const는 안 돼요
console.log(globalThis.appName, globalThis.version); // SIMPLE undefined
console.log('structuredClone' in globalThis); // true — 기능이 있는지 확인할 때
최상위 var와 함수 선언문이 전역 객체에 붙는 건 <script>로 불러온 일반 스크립트의 이야기예요. 모듈(type="module", 13장)에서는 파일마다 상자가 따로 있어서 전역에 붙지 않아요. 전역 이름은 다른 스크립트와 부딪힐 수 있으니 직접 만들지 않고, globalThis는 "이 기능이 있나?"를 확인할 때 주로 써요.
04. 즉시 실행 함수: 지금도 쓰는 곳, 대체된 곳
함수를 만들자마자 바로 부르는 모양을 즉시 실행 함수(IIFE)라고 해요. 함수 전체를 괄호로 감싸고 끝에 ()를 붙여요.
(function () {
var counter = 0; // 이 함수 안에서만 보여요 — 전역을 더럽히지 않아요
// …
})();
let · const와 모듈이 없던 시절, var는 블록을 무시하니 상자를 만들 방법이 함수뿐이었어요. 그래서 전역 이름이 새지 않게 파일 전체를 IIFE로 감쌌어요. 지금은 그 자리를 대부분 모듈과 블록이 대신해요. 그래도 아직 쓰는 곳이 있어요.
- 모듈이 아닌 일반 스크립트:
<script src>로 불러오는 파일은 지금도 IIFE로 감싸 전역 이름을 만들지 않아요. 이 블로그의 그림 파일(assets/scenes/*.js)도 모두 IIFE로 감싸져 있어요. - 최상위
await를 못 쓰는 곳:(async () => { … })()로 감싸 그 안에서await를 써요(11장). - 값을 계산해 한 번에 담기: 여러 줄의 계산 결과를
const에 바로 담고, 중간 변수는 밖으로 내보내지 않을 때 써요.
const shipping = (() => {
const base = 3000, freeFrom = 30000, total = 32000; // 중간 변수는 안에만
return total >= freeFrom ? 0 : base;
})();
console.log(shipping, typeof base); // 0 undefined
(async () => {
const value = await Promise.resolve('기다린 값');
console.log(value); // 기다린 값
})();
예전 코드를 옮겨 왔더니 상품마다 할인한 가격을 보여 줘야 할 함수 세 개가 모두 마지막 상품(지우개 560)만 보여 줘요. var를 고쳐서 ['연필 450', '공책 1600', '지우개 560']이 나오게 해 보세요.
var items = [['연필', 500], ['공책', 2000], ['지우개', 800]];
var rates = [0.1, 0.2, 0.3];
var labelers = [];
for (var i = 0; i < items.length; i++) {
var name = items[i][0], price = items[i][1], rate = rates[i];
labelers.push(function () {
return name + ' ' + price * (1 - rate);
});
}
console.log(labelers.map(f => f()));
해답 보기
var로 만든 name · price · rate는 함수 단위 상자 하나에 들어 있어서, 세 함수가 모두 마지막 회차의 값(지우개, 800, 0.3)을 봐요. 반복문 변수와 반복문 안의 변수를 let · const로 바꾸면 회차마다 새 상자가 생겨요.
const items = [['연필', 500], ['공책', 2000], ['지우개', 800]];
const rates = [0.1, 0.2, 0.3];
const labelers = [];
for (let i = 0; i < items.length; i++) {
const [name, price] = items[i];
const rate = rates[i];
labelers.push(() => name + ' ' + price * (1 - rate));
}
// ['연필 450', '공책 1600', '지우개 560']
05. 실전 포인트
- 언제 쓰나요? 새 코드에서
var는 쓰지 않아요. 오래된 코드를 읽거나 고칠 때 블록 무시 · 끌어올리기 · 반복문 클로저 버그를 의심해요. 기능 확인은'이름' in globalThis, 일반 스크립트 파일은 IIFE로 감싸요. - 대신 쓰는 것:
var대신const(기본) ·let, 파일 단위 상자는 IIFE 대신 모듈,window대신globalThis(Worker · 서버에서도 도는 코드라면)를 써요. ESLint의no-var규칙을 켜 두면 실수로 쓰지 않아요(ESLint). - 흔한 실수: 오래된 반복문을 그대로 옮겨 모든 콜백이 마지막 값을 보는 것, 선언 전에 읽은
var가undefined로 조용히 넘어가는 것, 최상위var가 다른 스크립트의 전역 이름과 부딪히는 것이에요.
정리
var는 블록을 무시하고 함수 단위로 상자를 만들어요. 다시 선언해도 오류가 없어요.- 반복문의
var는 상자 하나를 함께 써서, 안에서 만든 함수들이 모두 마지막 값을 봐요. var는undefined로, 함수 선언문은 통째로 끌어올려져요.- 전역 객체는 환경마다 이름이 달라요. 어디서나
globalThis로 써요. - IIFE는 모듈이 없던 시절의 상자 만들기예요. 일반 스크립트와 async 감싸기에 아직 써요.
if 블록 안에서 var x = 1로 선언하면 블록 밖에서 x를 읽을 수 있을까요?
네, 읽을 수 있어요. var는 블록이 아니라 함수 단위로 상자를 만들어서, 같은 함수 안이면 어디서나 보여요.
선언 전에 var 변수를 읽으면 오류가 날까요?
오류 없이 undefined가 나와요. 선언이 끌어올려져 미리 undefined로 만들어져 있기 때문이에요. let · const는 같은 상황에서 ReferenceError를 내 줘요.
브라우저 · Worker · Node.js 어디서나 전역 객체를 가리키는 이름은?
globalThis예요. window는 브라우저 화면에만, self는 브라우저와 Worker에, global은 Node.js에만 있어요.