var x = 'global'; // 1️⃣
function foo () {
var x = 'function scope'; // 2️⃣
console.log(x);
}
foo(); // ?
console.log(x); // ?
1️⃣ 전역에 선언되었기 때문에 어디에서든 참조할 수 있다.
2️⃣ foo
함수 안에서 선언되었기 때문에 foo
함수 내부에서만 참조 가능하다.
⇒ 이러한 규칙을 스코프라고 한다.
만약 스코프가 존재하지 않는다면, 같은 식별자 이름은 충돌을 발생시키기 때문에 프로그램 내에서 단 하나의 식별자 이름만 쓸 수 있다. 컴퓨터로 생각해보면 컴퓨터에 디렉토리가 존재하지 않았다면 같은 이름의 파일은 하나만 존재할 수 있다.
⇒ 스코프도 이와 같이 식별자 이름의 충돌을 방지한다.
// c언어
int main(void) {
// block-level scope
if (1) {
int x = 5;
printf("x = %d\n", x);
}
printf("x = %d\n", x); // use of undeclared identifier 'x'
return 0;
}
// javascript의 var
function main() {
if (true) {
var x = 5;
console.log(x);
}
console.log(x); // 5
}
ES6에서 도입된
let
같은 경우는 블록 레벨 스코프를 따른다. 즉,let
을 사용하면 c언어 예시와 같은 현상이 나타난다.
var
키워드로 선언한 전역 변수는 전역객체 window의 프로퍼티이다.함수 레벨 스코프는 어떤 문제가 있을까?
if (true) { var x = 5; } console.log(x); // 5
자바스크립트에서의
var
는 함수 레벨 스코프를 따르기 때문에 코드 블록 내에서 선언되었다 하더라도, 함수 블록이 아니라면 전역 스코프를 갖는다. 즉, 전역 변수로 선언될 확률이 높다.
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo();
bar();
위 코드의 실행 결과는 bar
함수의 상위 스코프가 전역(1️⃣)이냐 foo
(2️⃣)냐에 따라 값이 달라진다.
1️⃣ : 만약 bar의 상위 스코프가 전역이라면 이는 bar가 선언된 시점을 중점으로 스코프를 결정한 것이다.
2️⃣ : 만약 bar의 상위 스코프가 foo
라면 이는 bar가 실행된 시점을 중점으로 스코프를 결정한 것이다.
1️⃣의 방식을 정적 스코프 (static scope) 또는 렉시컬 스코프 (lexical scope)라고 하고,
2️⃣의 방식을 동적 스코프 (dynamic scope) 라고 한다.
자바스크립트를 포함한 여러 언어들이 정적 스코프 • 렉시컬 스코프를 따른다.
즉, 함수를 호출한 위치가 아닌 선언한 위치에 따라 스코프를 결정한다.
var x = 10; // 전역 변수
function foo () {
y = 20; // 선언되지 않은 식별자
console.log(x + y);
}
foo(); // 30
foo
안의 y
는 선언되지 않은 변수로 y = 20
을 실행하면 참조 오류가 날 것이라고 예상할 수 있다.
하지만 예상과 다르게 y
는 선언된 변수처럼 사용이 된다.
자바스크립트 엔진은 변수 y
에 값을 할당하기 위해 먼저 스코프 체인을 통해 선언된 변수인지 확인한다.
이때 스코프 어디에서도 변수 y
의 선언을 찾을 수 없으므로 참조 에러가 발생해야 하지만, 자바스크립트 엔진은 y = 20
을 window.y = 20
으로 해석하여 프로퍼티를 동적 생성한다.
이런 이유로 y
는 전역 객체의 프로퍼티가 되어 마치 전역 변수처럼 동작한다.
이러한 현상을 암묵적 전역(implicit global)이라 한다.