출처 모던 자바스크립트 Deep Dive을 보고 정리한 내용입니다.
스코프란 식별자가 유효한 범위를 말한다. 이전 자바스크립트의 함수에 관한 글에서 서술하였듯이 함수의 매개변수의 유효한 범위는 함수 몸체 내부이다. 즉, 함수의 매개변수의 스코프는 함수의 스코프인 것이다. 모든 식별자는 선언된 위치에 따라 자신을 참조할 수 있는 범위를 결정한다.
function add(num1, num2) {
console.log(num1 + num2);
return num1 + num2;
}
add(1, 2); //3
console.log(num1, num2) //RefernceError, num1, num2 변수는 add 함수 내에서만 유효함
아래 코드를 실행한 결과가 무엇일지 예측해보자.
var value = 1;
function foo() {
var value = 100;
console.log(value);
}
foo();
console.log(value);
자바스크립트 엔진은 스코프를 통해 참조해야할 식별자를 결정한다. 이를 식별자 결정(indetifier resolution)이라고 한다. 코드가 어디에 있는지에 따라 결과가 달라지며 이를 렉시컬 환경(lexical enviroment)이라고 한다. 그렇기 때문에 동일한 식별자를 가졌다고 하더라도 다른 결과값을 출력할 수 있다. 스코프의 종류는 전역 스코프와 지역 스코프라 나뉜다. 이를 알아보며 위 코드의 결과값을 알아보자.
전역이란 코드의 가장 바깥 영역을 말한다. 이 영역에서 선언된 변수를 전역 변수라고 말하며 전역 스코프를 갖는다. 코드 내 어디서든 참조할 수 있다. 위 코드에서 코드 최상단에 선언된 value는 전역 스코프에 선언된 식별자이다.
지역이란 함수 몸체 내부를 말한다. 지역은 지역 스코프를 만들고, 지역 내에서 선언된 변수를 지역 변수라고 말한다. 위 코드에서 foo함수 내의 value는 함수 스코프 내에서 선언된 식별자로 전역 스코프에 선언된 value와 식별자명은 같지만 다른 별개의 변수로 판단된다. 그러므로 foo함수가 호출되었을 때 100이라는 값을 출력하고, 함수를 탈출한 후 console.log문에서 쓰인 value는 전역 스코프에 선언된 값인 1이 출력된다.
function outer() {
var value1 = 'outer value1';
var value2 = 'outer value2';
function inner() {
var value1 = 'inner value';
console.log(value1); //'inner value'
console.log(value2); //'outer value2'
}
inner();
console.log(value); //outer value1
}
outer();
지역 변수는 자신의 지역 스코프와 하위 지역 스코프에서 유효하다. outer함수 내에서 선언된 지역변수인 value1과 value2는 outer 함수 내에 스코프에서 유효한 식별자이다. outer의 중첩함수인 inner 함수내에서 value1이라는 변수를 할당하면 inner 함수 내에서는 inner 함수 스코프 내에서는 'inner value'라는 값을 갖고 있는 식별자가 만들어진다. inner 함수 내에서 value2는 선언되어 있지 않지만 상위 스코프인 outer 함수 내에서 value2가 이미 할당되어 있으므로 참조가 가능하다. inner 함수의 value1과 outer 함수의 value1은 식별자만 같을 뿐 다른 변수이므로 outer함수에서 출력하는 value1의 값은 outer함수에서 초기화한 'outer value1'이라는 값을 출력한다.
위와 같은 과정은 자바스크립트 엔진에서 문맥을 통해 실행하며, 자바스크립트 엔진은 스코프 체인(scope chain)을 통해서 참조할 변수를 검색하는 과정을 갖는다.
스코프는 전역 스코프와 지역 스코프로 나뉜다. 지역 스코프는 함수 선언에 의해 만들어지며, 함수는 중첩함수의 형태로 구현될 수 있다. 즉, 지역 스코프는 중첩될 수 있다. 그렇기 때문에 스코프는 계층적 구조를 갖는다. 모든 지역 스코프의 최상위 스코프는 전역 스코프다. 위 코드에서는 전역 스코프 <- outer 지역 스코프 <- inner 지역 스코프의 계층적 구조를 갖는다. 변수를 참조할 때 스코프 체인을 통해 해당 코드의 스코프부터 시작하여 상위 스코프 방향으로 이동하며 변수를 검색한다. 그렇기 때문에 하위 스코프는 상위 스코프 내에서 선언된 변수를 참조할 수 있다. 위 과정은 렉시컬 환경에서 실제로 생성되며, 스코프 체인은 실행 컨텍스트의 렉시컬 환경을 단방향으로 연결한 것이다. 전역 렉시컬 환경은 코드의 로드와 함께 생성되며 함수의 렉시컬 환경은 함수가 호출되면 생성된다.
자바스크립트는 var키워드로 생성한 변수에대해 C언어와 Java와 같은 대부분의 언어와 다르게 함수 레벨에 의해 스코프를 생성하며 이를 함수 레벨 스코프라고한다. C, Java와 같은 언어는 코드 블럭을 통해 스코프를 생성한다. 그렇기 때문에 조건문, 반복문 또한 지역 스코프를 갖는다. 그러나 자바스크립트는 오직 함수에 의해서만 스코프가 생성된다.
var value = 1;
if(value === 1){
value = 100;
}
console.log(value); //100
var i = 100;
for(var i = 0; i < 5; i++) {
console.log(i); //0 1 2 3 4
}
console.log(i); //5
그러므로 위와 같은 코드에서 조건문과 반복문 내의 코드블럭에서 사용된 변수는 전역변수의 값을 변경한다.
var x = 1;
function foo() {
var x = 100;
bar();
}
function bar() {
console.log(x); //?
}
foo();
위 코드에서 bar함수의 호출 결과는 무엇일까? 두가지 케이스로 생각해볼 수 있다.
1번의 경우에는 bar 함수의 호출 시점에서 x의 값이 100이기 때문에 100을 반환할 것이다. 함수 호출 시점에 따라 동적으로 상위 스코프가 결정되기 때문에 이를 동적 스코프라고 한다.
2번의 경우에는 bar 함수의 정의 시점에서 x의 값이 1이기 때문에 1을 반환할 것이다. 이처럼 함수의 호출과 관계없이 함수의 정의가 평가된 시점에 상위 스코프가 결정되기 때문에 이를 렉시컬 스코프 또는 정적 스코프라고 한다.
자바스크립트는 렉시컬 스코프를 따른다. 그러므로 위 코드는 1을 호출할 것이다. 즉, 함수의 상위 스코프는 언제나 자신이 정의된 스코프이다.