
let, const 변수를 전역으로 선언하면 해당 스코프에 저장되는 것을 확인할 수 있다.var 변수는 Global에 저장된다.ES6 이후로는 var 쓸 일이 없으니, 사용자 정의 변수/함수는 Script Scope에 저장되어 JS 컴파일러에 의해 전역적으로 관리된다고 생각하면 될 것 같다.
이렇게 나눠진 이유로는,
이렇게 나눔으로써 조금이라도 구분하기, 접근하기 쉽게 하기 위한 것이 아닐까? 하는 추정이다.
var는 function scope이다.let, const는 block scope이다.{}형태의 모든 블록 내에서 지역적인 스코프를 갖는다.function scope에서는 var과 let/const는 동일하게 작동하지만 block에 대해서는 다르게 작동한다
예시를 통해서 둘을 비교해보자.
var i = 'I';
for(var i = 0;i < 1;i++){
//do something
}
console.log(i); // 1
let j = 'J';
for(let j = 0;j < 1;j++){
//do something
}
console.log(j); // J
var i, let j를 선언했고 둘 다 block scope인 for문에서 해당 변수를 사용하여 수행을 했다.var는 function scope이기 때문에 block에 대해서는 스코프를 지키지 못하고 재선언으로 인식되어, for문 수행이 끝난 뒤에는 for내부에서 선언했던 i값에 의해 값이 I에서 1로 변경이 된 것을 볼 수 있다.let은 block scope이기 때문에 block에 대해서도 스코프를 지키게 되고, for문 내부에서 선언된 지역 let변수 j는 해당 반복문이 수행이 끝난 후에는 해당 스코프 밖의 j까지 변경시키지 않는다.{
var x = 1;
}
console.log(x); // 1
{
let y = 1;
}
console.log(y); // ReferenceError: y is not defined
var는 function scope이므로 {}내에서는 지역변수로 역할을 하지 못하므로, 블록을 벗어나서도 접근이 가능한 것을 볼 수 있다. 즉, 해당 예시에서는 var는 global scope에서 정의한 것과 같은 의미가 되는 것이다.let block scope이므로 해당 블록을 벗어나자 정의되지 않은 것을 알 수 있다.더 자세한 차이는 추후 호이스팅과 TDZ를 통해 설명할 예정이다.
정적 스코프(Static Scope), 렉시컬(어휘적) 스코프(Lexical Scope)는 동일한 의미이다.
자바스크립트는 동적 스코프가 아닌, 정적 스코프 방식을 사용하는데, 이가 뜻하는 의미는 함수를 어디서 호출하는지가 아니라 어디에서 선언을 했는가에 따라 결정되는 것을 의미한다.
다시 말하면,
JS에서 정적 스코프란, 호출 지점이 결정하는 것이 아니라, 선언 지점이 결정한다는 것이다.
아래 예시를 한번 보자.
let l0 = 'l0';
function func2(){
let l2 = 'l2';
console.log(l0,l1,l2); //l1을 찾을 수 있을까? -> NO
}
function func1(){
let l1 = 'l1';
console.log(l0,l1); // console : l0 l1
func2();
}
func1();
과연 위 코드에서 func2()는 l1을 찾을 수 있을까? 찾을 수 없다.
정적 스코프이기 때문에 func2 선언 지점에서 해당 스코프 내에서 l1을 찾을 수 없고,
아무리 func1() 내부에서 호출하였다 하더라도 실행하면 런타임 에러가 발생한다.
function func3(){
function func4(){
let l2 = 'l2';
console.log(l0,l1,l2);//l1을 찾을 수 있을까? -> YES
//console : l0 l1 l2
}
let l1 = 'l1';
console.log(l0,l1); //console : l0 l1
func4();
}
func3();
반면 예시2는 어떨까? func4()는 func3() 내부에서 선언이 되었다.
func4()에서 l1을 참조할 때, 먼저 자신 내부 스코프에서 찾은 후, 없는 것을 확인하고는 본인의 상위 스코프인 func3()안에서의 l1을 찾게 된다.
따라서 문제없이 l0, l1, l2가 출력이 된다.
Closure
함수가 선언된 시점에서의 렉시컬 스코프(주변 상태)를 기억하여 내부(자식)함수가 외부(부모)함수를 참조하여 접근할 수 있도록 하는 개념이다.
내부함수에서 외부함수를 참조하여 사용하는 경우 이 내부함수, 일반화하면
자신이 선언이 될 당시의 렉시컬 스코프를 기억하는 함수 자체를 클로저라고 부른다.
위 1-2의 렉시컬 스코핑에서 설명한 것이 결국 클로저의 개념이다.
컴파일러가 예시2를 실행하는 과정을 한번 보자면,
func4가 호출되고, 내부에서 console.log(l0, l1, l2)를 마주한다.l2 찾기func4 내부 스코프)에 l2 : 'l2'를 통해 l2를 참조한다.l1 찾기l1이 없는 것을 확인한다. 상위 스코프로 이동하여 찾아본다.l1 : 'l1'을 통해 l1를 참조한다.l0 찾기l0가 없는 것을 확인한다. 상위 스코프로 이동하여 찾아본다.l0가 없는 것을 확인한다. 상위 스코프로 이동하여 찾아본다.l0 : 'l0'을 통해 l0를 참조한다.l0, l1, l2를 출력한다.해당 예시에서 func4()는 내부함수이며, 클로저인 것이다.
이런 방식으로 가장 내부 스코프를 먼저 탐색하고, 찾는 변수가 없을 경우 순차적으로 상위 스코프로 이동하며 스코프를 검색하는 것을 스코프 체인이라고 한다.
클로저는 생성된 시점의 스코프를 기억한다고 했는데,
이 특성으로 인해 외부 함수가 소멸이 되어도 참조를 기억하여 내부함수가 외부함수를 참조 할 수 있다.
말로만 하면 어려우니 아래 예시를 한번 보자.
function makeFunc() {
const name = "Kim";
function displayName() {
console.log(name);
}
return displayName;
}
const myFunc = makeFunc();
myFunc();
외부함수 makeFunc()는 내부함수 displayName()을 호출하기도 전에 내부함수에 대한 참조값만 리턴 값으로 남기고 선언이 끝이 났다.
여기서 잠깐,
- displayName() 형식으로 괄호를 붙여야 함수가 즉시 호출되어 내부 함수가 실행된다.
- 괄호 없이 함수 이름 'displayName'만 리턴하는 것은 함수를 참조만 하는 것이다.
클로저의 개념이 없었다고 한다면, (or 다른 프로그래밍 언어에서는,)
내부함수가 수행 할 동안만 지역변수(name)가 존재하여 makeFunc() 실행이 끝난 뒤에는 name에 대해 접근이 불가능 할 것 같지만,
Javascript는 클로저를 형성하기 때문에
displayName은 클로저로서 선언 당시의 name에 대한 참조를 기억하고 있다.
스코프 체인을 따라가보면,
myFunc는 displayName에 대한 참조 값을 담고 있기 때문에
myFunc() : displayName() 호출displayName() : Closure (makeFunc)의 name 참조 → 콘솔에 name 출력과정을 거쳐 makeFunc의 선언이 종료된 상태에서도 내부의 name 변수에 참조할 수 있는 것이다.