스코프란 유효 범위라고도 부르며, 변수가 어느 범위까지 참조되는 지를 뜻한다. 변수는 자신이 선언된 위치에 따라 다른 코드가 변수 자신을 참조할 수 있는 범위가 결정된다. 변수 뿐만 아니라 모든 식별자에 해당되므로 스코프는 식별자가 유효한 범위를 뜻한다. 스코프는 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙이다.
전역이란 코드의 가장 바깥 영역을 말한다. 전역에 변수를 생성하면 전역 변수가 되며 전역 변수는 어디에서든지 참조할 수 있다.
지역이란 함수 몸체 내부를 뜻한다. 지역은 지역 스코프를 만들며 지역에 선언된 지역 변수는 자신이 선언된 지역과 하위 지역에서만 참조할 수 있다.
var var1 = 1; // 전역 변수
function foo() {
var var2 = 2; // 지역 변수
function bar() {
var var3 = 3; // 지역 변수
}
}
console.log(var1); // 1
console.log(var2); // ReferenceError: var2 is not defined
console.log(var3); // ReferenceError: var3 is not defined
var1
은 코드의 가장 바깥에 선언되었으므로 전역 변수이다.
var2
와 var3
은 함수 내부에 선언된 지역 변수로 두 변수는 지역 스코프의 외부에서는 참조할 수 없다.
모든 스코프는 상/하위로 하나의 계층적 구조로 연결된다. 모든 지역 스코프의 최상위 스코프는 전역 스코프이다. 이처럼 스코프가 계층적으로 연결된 것을 스코프 체인이라고 한다.
변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작해서 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.
var x = 'global';
function outer() {
var y = 'local';
console.log(x); // global
console.log(y); // local
function inner() {
var x = 'local';
console.log(x); // local
console.log(y); // local
}
inner();
}
outer();
console.log(x); // global
console.log(y); // ReferenceError: y is not defined
inner
함수 내부에서 참조되는 변수 x
는 inner
함수의 지역 스코프에서 변수 x
가 선언됐는지 검색한다. 변수 x
가 존재하므로 변수 x
의 값이 출력된다. inner
함수 내부에서 참조되는 변수 y
도 마찬가지로 inner
함수 내부에서 변수 y
를 검색해보고 없으면 상위 스코프인 outer
함수의 지역 스코프로 이동한다.
이처럼 자바스크립트 엔진은 스코프 체인을 따라 변수를 참조하는 코드의 스코프에서 상위 스코프로 이동하며 변수를 검색한다. 상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조할 수 있지만 하위 스코프에서 유효한 변수를 상위 스코프에서 참조하는 것은 불가능하다.
예시는 변수로 들었지만, 스코프는 식별자를 검색하는 규칙에 해당되므로 함수 등도 똑같이 적용된다.
var x = 'global';
function foo() {
console.log(x); // (1)
var x = 'local';
}
foo();
console.log(x); // global
foo
함수 내부에서 선언된 지역 변수 x
는 (1)
시점에 이미 선언되었고, undefined
로 초기화 되어 있다. 따라서 지역 변수 x
를 참조한다. 이처럼 호이스팅은 스코프를 단위로 동작한다.
함수 레벨 스코프는 함수에 의해서만 지역 스코프가 생기는 것이다. var
키워드로 선언된 변수는 오로지 함수의 코드 블록만을 지역 스코프로 인정한다.
블록 레벨 스코프는 모든 코드 블록(if
, for
, while
, {}
...)이 지역 스코프를 만든다.
var var1 = 1;
if (true) {
var var2 = 2;
if (true) {
var var3 = 3;
}
}
function foo() {
var var4 = 4;
function bar() {
var var5 = 5;
}
}
console.log(var1); // 1
console.log(var2); // 2
console.log(var3); // 3
console.log(var4); // ReferenceError: var4 is not defined
console.log(var5); // ReferenceError: var5 is not defined
이 예시에서 모든 변수는 var
키워드를 통해 선언되었다. var
은 함수 레벨 스코프이기 때문에 함수 몸체만 지역 스코프로 여긴다. 따라서 if
문 내부에 선언된 변수도 전역 변수에 속하므로 참조할 수 있다. 그러나 함수 foo
, bar
내부에 선언된 변수들은 하위 스코프이기 때문에 참조가 불가능하다.
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000)
}
이 코드는 로그에 5가 5번 찍힐 것이다. 이유는 var
키워드로 이루어진 변수 선언은 함수 스코프이기 때문에 setTimeout
이 이미 실행될 시점에 i
는 이미 5가 되어있고 모든 함수가 같은 값 i
를 참조한다.
이를 고치기 위해서는 블록 레벨 스코프인 let
을 사용해야 한다.
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000)
}
함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정하는 방법을 렉시컬 스코프(정적 스코프)라고 한다. 함수 정의가 평가되는 시점에 상위 스코프가 결정된다. 따라서 함수의 상위 스코프는 언제나 자신이 정의된 스코프이다. 함수 정의가 실행되어 생성된 함수 객체는 이렇게 결정된 상위 스코프를 기억한다.
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // 1
bar(); // 1
bar
함수는 전역에서 정의되었으므로 전역 스코프를 기억한다. 따라서 bar
함수는 전역 스코프를 상위 스코프라고 생각하므로 두 함수 모두 1을 출력한다.
let x = 30;
function get() {
return x;
}
let result = get(20); // 30
함수 get
은 x
를 리턴하는데 이 x
는 전역 변수의 x
를 가르킨다. get
함수 스코프 안에 x
라는 변수가 별도로 선언되어 있지 않기 때문이다. 20은 인자로 받아 함수에 전달되었지만 함수 내부에 어떤 변수에도 할당되지 않는 즉, 사용되지 않는 값이다.
let x = 30;
function get(x) {
return x;
}
let result = get(20); // 20
함수에 매개변수 x
가 선언되었으므로 20의 값을 받아 리턴할 수 있다. 즉 함수가 리턴하는 x
의 값은 전역 변수가 아니라 함수 스코프에 선언된 별도의 매개변수인 x
이다.
let x = 30;
function get () {
return x;
}
function set (value) {
x = value;
}
set(10);
let result = get(20); // 10
set
함수에 있는 x
값은 전역 변수이므로 set(10)
에 의해 전역변수가 x = 10
으로 재할당 된다. get
함수에는 20을 받을 매개변수가 없으니 전역변수 x
를 그대로 반환해주며 set
함수에 의해 재할당된 10을 리턴하게 된다.
let x = 10;
function outer () {
let x = 20;
function inner () {
return x;
}
return inner();
}
let result = outer(); // 20
outer
함수 스코프에는 20을 값으로 하는 변수 x
와 함수 inner
가 선언되어 있다. inner
함수는 x
를 반환한다. 그러나 inner
함수 스코프 내에 x
라는 변수가 없기 때문에, 상위 스코프인 outer
함수 스코프로 이동해서 변수 x
를 검색한다.
let x = 10;
function outer () {
let x = 20;
function inner () {
x = x + 10;
return x;
}
inner();
}
outer();
let result = x; // 10
변수 result
에 할당된 값은 전역 스코프의 x
이므로, result
의 값은 outer
함수나 inner
함수 스코프에 의해 영향을 받지 않는다. 또한 inner
함수가 참조하는 x
값은 상위 스코프인 outer
함수에 정의된 x
이다.
let x = 10;
function outer () {
x = 20;
function inner () {
let x
x = x + 20;
return x;
}
inner();
}
outer();
let result = x; // 20
outer
함수에서 x = 20
코드에서 참조하고 있는 x
는 outer
함수에 존재하지 않으므로 그보다 상위 스코프인 전역 스코프에 선언된 x
이다. 따라서 전역변수 x
의 값이 20으로 재할당 된다. inner
함수 스코프의 변수 x
값과 outer
함수 스코프의 변수 x
값이 참조하는 값은 다르다.
let x = 10;
function outer () {
x = 20;
function inner () {
x = x + 20;
}
inner();
}
outer();
let result = x; // 40
모든 함수 안에 있는 x
의 값은 참조하는 코드가 있는 스코프에 존재하지 않으므로 상위 스코프로 이동하며 변수를 검색한다. 따라서 전역 스코프에 선언된 전역 변수 x
를 참조한다. 따라서 outer()
를 통해 20으로, inner()
를 통해 40으로 바뀌어 result
에 할당된다.