변수 선언 위치에 따른 작동 방식의 차이와 사용 여부
5.1 변수 사용 가능 시점
모든 식별자는 컴파일 타임 때 각자의 스코프에 등록된다. 또 모든 식별자는 자신이 속한 스코프가 생성될 때 해당 스코프의 시작 부분에서 생성된다.
이렇게 선언은 스코프 아래에 있더라도 스코프 시작부분에서 변수의 가시성이 확보되는 걸 호이스팅이라고 한다.
하지만 가시성을 넘어 함수의 경우 실제로 실행이 되기도 하는데, 이는 함수 호이스팅 때문이다. 함수 선언문으로 함수를 선언하면 함수 이름에 해당하는 식별자가 스코프 최상단에 등록되고, 함수 참조로 그 값이 자동으로 초기화된다. 따라서 함수는 스코프 어디서든 호출 가능해진다.
1
2
3
hello();
function hello() {};
함수 호이스팅, var 호이스팅 모두 블록 스코프가 아닌 가장 가까운 함수 스코프에 등록된다. (함수가 없으면 전역으로)
선언문과 표현식에서의 호이스팅 차이
1
2
3
hello(); // TypeError
var hello = function hello() {}
- var로 선언한 변수는 호이스팅이 되지만, 스코프가 시작될 때 undefined로 초기화됨. 코드상 할당문에 도달하기 전까지 undefined로 계속 남아있음. 따라서
TypeError
가 난다. (참조는 가능하기에ReferenceError
는 나지 않음.) - 함수 선언문의 경우 호이스팅이 되고 해당 함숫값으로 초기화되는 것과는 동작이 다르다
변수 호이스팅
1
2
3
4
greeting = "hi";
console.log(greeting) // hi
var greeting = "hello";
- var는 호이스팅되며 undefined로 초기화되기에, 첫번째줄에서부터 값을 할당할 수 있다ㅏ
5.2 호이스팅 : 비유일 뿐입니다
호이스팅은 아래처럼 코드를 재정렬하는 것으로 생각하기가 쉽다.
1
2
3
4
5
var greeting; // 호이스팅됨
greeting = "hi";
console.log(greeting) // hi
greeting = "hello";
하지만 실제 JS 엔진은 코드를 재정렬하지 않는다. JS 엔진은 앞을 내다보고 선언문을 찾지 못한다. 프로그램의 모든 스코프 경계와 선언문을 정확히 찾는 것은 파싱 과정이다.
물론 호이스팅을 코드 재정렬로 생각할 수 있다. 이는 이해에 도움을 주지만, 호이스팅은 런타임이 아닌 컴파일 단계에서 일어나는 일임을 기억해야한다
5.3 중복 선언 처리하기
- var 중복 선언 : 두번째 선언이 무시됨
1
2
3
4
var name = "name"
console.log(name); // "name"
var name;
console.log(name); // "name"
아래와 같이 이해할 수 있음.
1
2
3
4
5
6
7
var name;
var name; // 의미 없는 작업
name = "name";
console.log(name); // "name"
console.log(name); // "name"
이때 var name;
과 var name = undefined;
는 큰 차이가 있다. 만약 후자처럼 선언하였으면 다음과 같이 동작한다.
1
2
3
4
5
6
7
var name;
var name; // 의미 없는 작업
name = "name";
console.log(name); // "name"
name = undefined;
console.log(name); // "name"
- 함수와 var의 중복선언 : 함수 호이스팅이 변수 호이스팅보다 우선순위가 높음.
1
2
3
4
5
6
7
8
9
10
11
12
13
var hello;
function hello() {
console.log("hello");
}
var hello; // 의미 없는 작업
typeof hello; // "function"
var hello = "hi"
typeof hello; // "string"
- let 중복 선언 : SyntaxError 발생하며 실행이 안됨. (하나가 let이고 나머지가 var 인 경우도 안됨)
1 2
let name = "name"; let name = "name2";
let 중복 선언은 기술적으로는 가능하지만 TC39에서 스타일 상 막아놓은 것이다.
const 재선언
const도 let과 마찬가지로 동일한 스코프 내 동일 식별자를 사용할 수 없다. 하지만 const는 let과 달리 재선언을 기술적인 이유로 막아놓은 것이다
const는 변수를 선언할 때 할당도 필요하다. 만약 선언 시 할당을 빼놓으면 다음과 같이 SyntaxError
가 발생한다. 또한 재할당도 불가능하다
1
const empty; // SyntaxError
따라서 만약 const를 재선언하게 되면 어떤 형식이든 SyntaxError 가 발생한다
1
2
3
4
5
const name = "name1";
const name; // SyntaxError
const name = "name2"; // SyntaxError
name = "name3" // TypeError
SyntaxError vs TypeError SyntaxError 는 프로그램 실행전에 발생하여 프로그램을 실행시키지 않지만, TypeError는 프로그럼 실행 중 발생한다
반복문
스코프 규칙 (let으로 생성한 변수 재선언 등)은 각 스코프 인스턴스마다 적용된다.
반복문에서는 새로운 반복이 시작될 때마다 자체적인 새 스코프가 생성된다. 따라서 다음과 같은 케이스는 let 재선언이 일어나지 않고, 매번 새로운 let을 선언하는 것이고 에러가 발생하지 않는다. (let은 블록 스코프이기에)
1
2
3
4
5
6
7
var keepGoing = true;
while(keepGoing) {
let value = Math.random();
if(value > 0.5) {
keepGoing = false;
}
}
하지만 만약 var로 선언한 경우, var는 함수 스코프이기에 전역스코프에 연결이 되어 재선언은 일어나지 않지만, 재할당이 매번 일어난다.
1
2
3
4
5
6
7
var keepGoing = true;
while(keepGoing) {
var value = Math.random();
if(value > 0.5) {
keepGoing = false;
}
}
for문의 경우도 마찬가지인데, for 조건문 안에 있는 변수들도 매번 새로운 스코프에 연결되므로 에러가 발생하지 않는다.
1
2
3
for(let i = 0; i < 3; i++) { // 조건문 속 let은 매번 새로운 인스턴스와 연결됨.
let value = i * 30;
}
const의 경우도 마찬가지인데, 다만 특정 형태의 for 문에서는 문제가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var keepGoing = true;
while(keepGoing) {
const value = Math.random(); // 문제 없음
if(value > 0.5) {
keepGoing = false;
}
}
for(const student of students) { // 조건문 속 const는 매번 새로운 스코프와 연결됨
}
for(const i = 0; i < 3; i++) { // TypeError 발생
let value = i * 30;
}
이는 반복문 종료 이후, const 로 선언한 i
에 i++
라는 재할당이 발생하기에 에러가 발생하는 것이다.
5.4 초기화되지 않은 변수와 TDZ
TDZ : 변수는 존재하지만 초기화되지 않아 어떤 방식으로도 해당 변수에 접근할 수 없는 시간대. 초기화가 이루어지면 TDZ는 종료되고 스코프 내에서 변수를 자유롭게 사용할 수 있다.
아래와 같은 코드는 프로그램 실행시 ReferenceError가 발생한다.
1
2
3
console.log(name);
let name = "eil";
let, const로 선언된 변수는 호이스팅은 되지만, 스코프 맨 위에서 초기화가 발생하지 않기에 에러가 발생하는 것이다. 컴파일러는 프로그램 중간에서 해당 선언을 초기화하는 명령을 내리기에, 이 초기화가 발생하기 전까지는 TDZ에 걸려 변수를 사용할 수 없다. (엄밀히는 var도 TDZ가 있지만 길이가 0이다.)
let, const로 선언된 변수도 호이스팅은 되지만, 자동 초기화가 이뤄지지 않는다는 것만 다르다. 호이스팅 되는 것은 아래 코드가 에러를 발생시킨다는 것으로 알 수 있다.
1
2
3
4
5
var studentName = "카일"
{
console.log(studentName); // TDZ 에러발생
let studentName = "지수";
}