자바스크립트를 배워보자 13일차 - let과 const, 블록 레벨 스코프

0

Javascript

목록 보기
13/30

let, const키워드와 블록 레벨 스코프

1. var 키워드로 선언한 변수의 문제점

1.1. 변수 중복 선언 허용

var키워드로 선언한 변수는 중복 선언이 가능하다.

var x = 1;
var y = 1;

var x = 100 // 중복 선언
var y; // 중복 선언
console.log(x); // 100
console.log(y); // 1

위의 예는 var키워드로 선언한 변수는 중복 선언이 가능하다는 것을 보여준다.

신기하게도, var y; 가 실행되어 undefined가 있을 것 같지만, 아니다.

이는 중복 선언에 있어서 초기화문이 있는 변수 선언문은 자바스크립트 엔진에 의해 var이 없는 것처럼 동작하지만, 초기화 문이 없는 선언문은 무시된다. 이 때 에러는 없다.

var x = 1;
var y = 1;

x = 100 // 중복 선언
// 무시 => var y; 
console.log(x); // 100
console.log(y); // 1

즉, 다음과 같이 자바스크립트 엔진이 변환해준다는 것이다.

이러한 문제는 변수 선언이 이미 있는데, 다시 선언을 하여 선언한 값을 변경한다는 치명적인 단점이 있다.

1.2. 함수 레벨 스코프

var 키워드로 선언한 변수는 오로지 함수의 코드 블록만을 지역 스코프로 인정한다. 따라서 함수 외부에서 var 키워드로 선언한 변수는 코드 블록 내에서 선언해도 모두 전역 변수가 된다.

var x = 1;
if(true){
    var x = 10;
} // var은 if문과 같이 함수가 아닌 코드 블럭인 경우는 스코프를 같지 않는다. 따라서 전역 스코프가 적용되어 x값이 바뀌게 된다.
console.log(x);

for문의 변수 선언문에서도 var 키워드로 선언한 변수는 전역 변수가 된다.

var i = 10;
for(var i = 0; i < 5; i++){
    console.log(i); // 1 2 3 4 5
}
console.log(i) // 5

이러한 함수 레벨 스코프만 적용이되는 var 키워드는 전역 변수를 남발할 가능성을 높여 의도치않은 에러를 발생시킬 수 있다.

1.3. 변수 호이스팅

var 키워드로 변수를 선언하면 변수 호이스팅에 의해 변수 선언문이 스코프의 선두로 끌어 올려진 것처럼 동작한다.

즉, 호이스팅에 의해 var 키워드로 선언한 변수는 변수 선언문 이전에 참조할 수 있다는 것이다.

단, 할당문 이전에 변수를 참조하면 undefined만 나오게 된다.

console.log(foo);
foo = 123;

console.log(foo);
var foo;

변수 선언은 런타임 이전에 자바스크립트 엔진에 의해 실행된다. 즉 호이스팅 된다는 것이다.

이는 오류를 만들 여지를 남기는 것이다.

2. let 키워드

위에서 살펴본 var 키워드의 문제가 이만저만이 아니다. 따라서 이러한 문제를 보완하기 위해 let, const를 es6에서 도입하였다.

2.1. 변수 중복 선언 금지

let 키워드로 이름이 같은 변수를 중복 선언하면 문법 에러가 발생한다.

var foo = 123;
var foo = 456;

let bar = 123;
let bar = 456; // SyntaxError: Identifier 'bar' has already been declared

2.2 블록 레벨 스코프

var 키워드로 선언한 변수는 오로지 함수의 코드 블록만을 지역 스코프로 인정하는 함수 레벨 스코프를 따른다.

하지만 let 키워드로 선언한 변수는 모든 코드 블록(함수, if문, for문, while 문, try/catch 문 등) 을 지역 스코프로 인정하는 블록 레벨 스코프를 따른다.

let foo = 1;
{
    let foo =2;
    let bar = 3;
}
console.log(foo); // 1
console.log(bar); // ReferenceError: bar is not defined

let 키워드로 선언된 변수는 블록 레벨 스코프를 따르게 된다.

따라서, {} 안에 있는 foo와 bar 변수는 지역 변수가 된다.

전역에서 선언된 foo 변수와 코드 블록 내에서 선언된 foo 변수는 다른 별개의 변수이다.

이 때문에 지역 변수의 bar과 foo를 참조하지 못하는 것이다.

함수도 코드 블록이므로 스코프를 만든다. 함수 내의 코드 블록은 함수 레벨 스코프에 중첩된다.

let i = 10;
function foo(){
    let i = 100;
    for(let i = 1; i < 3; i++){
        console.log(i); // 1 2
    }
    console.log(i); // 100
}
foo();
console.log(i); // 100

let i 가 있는 곳은 전역 스코프이다.
foo함수 안은 함수 레벨 스코프이다.
foo함수 안의 for문은 블록 레벨 스코프이다.

따라서, 각자의 {} 블록 안에서만 유효한 값을 갖게 되는 것이다.

물론 다음과 같은 경우는 스코프 체이닝에 의해 전역 변수를 찾게 된다.

let i = 10;
function foo(){
    for(i = 1; i < 3; i++){
        console.log(i); // 1 2
    }
    console.log(i); // 3
}
foo();
console.log(i); // 3

2.3 변수 호이스팅

var키워드로 선언한 변수와 달리 let키워드로 선언한 변수는 변수 호이스팅이 발생하지 않는 것처럼 동작한다.

console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
let foo;

var 키워드로 선언한 변수는 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 '선언 단계'와 '초기화 단계'를 거친다. 이후 런타임으로 가게되어 '할당 단계'를 거치는 것이다.

  • 런타임 이전

    • 선언 단계
      선언 단계에서 스코프(실행 컨텍스트의 렉시컬 환경)에 변수 식별자를 등록해 자바스크립트 엔진에 변수의 존재를 알린다.

    • 초기화 단계
      이 다음 즉시 초기화 단계에 가게되어, undefined로 변수를 초기화한다.

  • 런타임 이후

    • 할당 단계
      이후 코드가 한 줄 씩 실행되면서 할당문에 도달하면 실행 컨텍스트의 변수 식별자를 찾아 값을 할당해준다.

그러나 let키워드로 선언한 변수는 "선언 단계"와 "초기화 단계"가 분리되어 진행된다

즉, 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 선언 단계가 먼저 실행되지만 초기화 단계는 변수 선언문에 도달했을 때 실행 된다.

즉, 할당 단계와 초기화 단계가 동일시되었다고 볼 수 있다.

만약 초기화 단계가 실행되기 이전에 변수에 접근하려고 한다면 참조 에러가 발생한다.

let키워드로 선언한 변수는 스코프의 시작 지점부터 초기화 단계 시작 지점(변수 선언문)까지 변수를 참조할 수 없다.

스코프의 시작 지점부터 초기화 지점까지 변수를 참조할 수 없는 구간을 일시적 사각지대(Temporal Dead Zone) 이라고 한다.

console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
let foo;
console.log(foo); // undefined
foo = 1;
console.log(foo); // 1

런타임 이전에는 선언 단계가 이루어졌지만, 초기화 단계가 이루어지지 않아 foo함수는 TDZ인 일시적 사각지대 상태에 빠지게 된다.

따라서 변수 foo를 접근하지 못하게 되는 것이다.

이후에 let foo; 가 런타임에 실행되면서 undefined가 default로 초기화되는 것이다.

그리고 할당문인 foo = 1 이 실행되어 1이 들어가는 것이다.

따라서 let의 단계를 나누면 다음과 같다.

------------------
|선언단계         |
------------------
|TDZ(일시적 사각지대)
------------------
|초기화단계       |      
------------------
|할당단계         |
------------------

그렇다면 let은 호이스팅이 작용되는것일까 안되는 것일까??

let foo = 1;
{
    console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
    let foo =2;
}

다음의 예제를 보면 코드블럭 {} 에서 foo를 부르면 스코프 체이닝에 의해 전역 변수 foo를 부를 것 같다.

그러나, 이는 잘못된 예측이다.
왜냐하면 지역 변수 foo가 선언 단계에서 호이스팅되어 블럭 레벨 스코프에 등록되기 때문이다.

따라서 위에서 console.log(foo)에서 부르는 foo는 전역 변수 foo가 아니라, 지역 변수 foo이기 때문에 TDZ에 있어 참조가 안되는 것이다.

따라서, let을 사용해도 호이스팅이 이루어 진다는 것을 알 수 있다.

참고로 더 나아가서 let, const를 포함해서 모든 선언 var, let, const, function, function*, class 등을 모두 호이스팅한다.

단, es6에서 도입된 let, const, class를 사용한 선언문은 TDZ를 이용하여 호이스팅이 발생하지 않은 것처럼 동작한다.

3. 전역 객체와 let

var키워드로 선언한 전역 변수와 전역 함수, 그리고 선언하지 않은 변수에 값을 할당한 암묵적 전역은 전역 객체 window의 프로퍼티가 된다.

왜 선언하지 않은 변수에 값을 할당한 암묵적 전역도 전역 객체인 window객체에 할당되냐고 한다면, windwo객체 를 생략할 수 있기 때문이다.

var x = 1; // 전역 변수
y = 2; // 암묵적 전역
function foo(){}
console.log(window.x); // 1
console.log(x) // 1;
//암묵적 전역은 전역 객체 window의 프로퍼티이다.
console.log(window.y); // 2;
console.log(y) // window는 생략이 가능하다.
console.log(window.foo) // foo(){}
console.log(foo) // foo(){}

그러나 let 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 아니다.

즉, let foo; 로 선언하면 window.foo로 접근할 수가 없다는 것이다.

참고로 let 전역 변수는 보이지 않는 개념적인 블록(전역 렉시컬 환경의 선언적 환경 레코드)내에 존재한다.

let x = 1;
console.log(window.x) // undefined 
console.log(x) // 1

4. const 키워드

const 키워드는 상수를 선언하기 위해서 사용한다. 하지만 반드시 상수만을 위해 사용하지는 않는다. 이에 대해서는 후반부에 다시 알아보도록 하자

const 키워드의 특징은 let키워드와 거의 유사하다.

const도 let과 같이 블록 레벨 스코프를 가지며, 변수 호이스팅이 발생하지 않는 것처럼 동작한다.

const만의 특징을 살펴보도록 하자

4.1. 선언과 초기화

const로 선언한 변수는 반드시 선언과 동시에 초기화해야 한다.

const foo = 1;
const w; // SyntaxError: Missing initializer in const declaration

그렇지 않으면 에러가 발생한다.

4.2. 재할당 금지

var 또는 let키워드로 선언한 변수는 재할당이 자유로우나 const 키워드로 선언한 변수는 재할당이 금지된다.

const foo = 1;
foo = 2; // TypeError

4.3. 상수

const 키워드 선언한 변수에 원시 값을 할당한 경우 값을 변경할 수 없다.

이는 여타 프로그래밍 언어와 같은 특징인 상수를 말한다.

4.4 const 키워드와 객체

const키워드로 선언된 변수에 원시 값을 할당한 경우(Number, string 등등)은 값을 변경할 수 없다.

하지만 const키워드로 선언된 변수에 객체를 할당한 경우 값을 변경할 수 있다.

const person = {
    name : "lee"
};
person.name = "kim";
console.log(person.name); // kim

const 키워드는 재할당을 금지할 뿐 "불변"을 의미하지 않는다

즉, 새로운 값을 재할당하는 것은 불가능하다

const person = {
    name : "lee"
};
person = {
    age: 20,
}; // TypeError: Assignment to constant variable.
console.log(person.age);

이 처럼 객체를 가리키는 참조 변수의 값을 바꿀 수 없다.

그러나, 참조 변수가 가리키는 객체의 프로퍼티를 추가, 변경, 삭제하는 것은 가능하다.

이는 주소에 접근하여 객체를 바꾸는 것이지 주소를 바꾸는 것이 아니기 때문이다.

0개의 댓글