ES6 block-scope & let, const
이 글은 [인프런] Javascript ES6+ 제대로 알아보기 - 초급(정재남)를 토대로 작성되었음을 알려드립니다.
ES6 에서 추가된 블록 스코프(block-scope)와 let, const 에 대해 살펴보도록 하겠습니다.
Block Scope
ES5 까지는 "자바스크립트에는 블록 스코프가 없다"고 알려져 있었으나 이제는 그렇지 않습니다.
ES5 에서는 함수 스코프가 함수에 의해서 생기는 변수의 유효범위였다면 블록 스코프(block-scope)는 무엇일까요?
블록 스코프의 스코프도 알다시피 유효범위를 의미하는데 그렇다면 블록이란? 바로 코드 문단인 { }
즉, 중괄호를 가리킵니다.
다시 말해, { }
에 의해서 생기는 변수의 유효범위,
{ } 에 의해서 변수의 유효범위가 결정된다고 할 수 있습니다.
ES6's block-scope
위 개념을 생각하면서 아래의 코드를 살펴보도록 합니다.
{
let a = 10;
{
let a = 20;
console.log( a );
}
console.log( a );
}
console.log( a );
이전 ES5 에서는 var
키워드로 변수를 선언했다면 ES6 부터는 let
, const
란 키워드가 생겨나 변수 선언시 let
,
const
키워드를 사용할 수 있습니다.
지난 시간에 살펴본 스코프를 생각하면 콘솔의 결과값이 다르다는 것을 확인하실 수 있습니다.
위의 블록 스코프 코드를 함수 스코프와 비교해 보도록 하겠습니다.
(function () {
var a = 10;
(function() {
var a = 20;
console.log( a );
})();
console.log( a );
})();
console.log( a );
ES6 의 블록 스코프와 함수 스코프와 콘솔 결과값을 비교하면 같습니다.
함수 스코프를 이해하셨다면 함수 외부에 a
란 변수가 전혀 선언되지 않았기 때문에 마지막의 콘솔 결과값이 a is not defined 가 왜 출력되는지 알 수 있을 것입니다.
그리고 함수 안에서 선언한 내부 함수에서 var a = 20;
은 함수가 정의된 유효범위에서만 존재하므로 외부함수에서 콘솔값은 10 이 나오게 되는 것입니다.
즉, 블록 스코프는 함수 스코프와 같은 개념으로 볼 수 있습니다.
함수 스코프에서 var 키워드를 사용한 경우와 let 키워드를 사용한 경우를 살펴보도록 하겠습니다.
var
키워드를 사용한 경우
function hasValue(p) {
console.log( v );
if (p) {
var v = 'blue';
console.log( v );
} else {
var v = 'red';
console.log( v );
}
console.log( v );
}
hasValue(10);
var
로 선언하면 블록 스코프의 영향을 받지 않습니다. 즉, 기존 ES5 에서 문법을 그대로 따르기 때문에 예상한 값들이 출력될 것입니다.
let
키워드를 사용한 경우
function hasValue2(p) {
console.log( v );
if (p) {
let v = 'blue';
console.log( v );
} else {
let v = 'red';
console.log( v );
}
console.log( v );
}
hasValue2(10);
전 예제 코드에서 var
로 선언한 것과 다르게 let
을 선언하면 마지막 콘솔값은 v is not defined 라고 출력됩니다.
블록 스코프와 let, const
위에서 살펴보면서 눈치챘듯이 블록 스코프는 let, const 에 대해서만 적용됩니다.
위의 내용을 상기해 보면서 결과를 예측해 보기 바랍니다.
var
키워드를 사용한 경우
console.log( a );
if (true) {
var a = 10;
if (true) {
var a = 20;
console.log( a );
}
console.log( a );
}
console.log( a );
let
, const
키워드를 사용한 경우
console.log( a );
if (true) {
let a = 10;
if (true) {
const a = 20;
console.log( a );
}
console.log( a );
}
console.log( a );
블록 스코프와 호이스팅 그리고 TDZ(Temporal Dead Zone)
블록스코프는 중괄호에 의해서 영향을 받는다(단, 함수에서의 중괄호는 이미 ES5 에서 함수 스코프가 존재하니 제외)고 언급했습니다.
다음과 같은 if, for, while, switch-case은 중괄호를 가지고 있습니다.
if (true) {
} // 실행하고 끝.
for (var i = 0; i < 10; i++) {
} // 순회하고 넘어가면 끝.
while (true) {
}
switch (a) {
case: break;
}
위와 같은 것들을 우리는 if문, for문, while문, switch-case문이라고 명명하고 있으며, 이러한 문은 '문단'은 약어입니다.
이러한 문과 반대되는 개념이 '식(expression)'이라고 하는데 이 식을 값이 될 수 있는 경우를 말합니다.
예를 들어, (10 + 20)
, 'abc' + 'def'
, function a() {return 1;} 에서 함수를 호출했을 때 a()
와 같이 값이 될 수 있는 것을 식이라고 합니다.
일반적으로 값, 식, 문의 나눌 수 있는데 값과 식은 동일하게 간주됩니다.
여기서 문이라고 하는 if문과 같은 '문단'은 결과를 리턴하지 않는다 것이 큰 차이점입니다.
다시 말해, 결과가 존재할 수 없습니다.
if 문을 실행해도 if 문에서 실행한 코드의 값을 담아놓을 수 없습니다. 실행하면 그걸로 끝입니다.
for 문도 마찬가지로 순회하고 다음 행으로 넘어갑니다. while 문, switch-case 문도 마찬가지입니다.
결론은 문 자체가 하나의 block-scope가 되는 것입니다.
그리고 ES6 문법을 포함하면 스코프는 함수 스코프와 블록 스코프가 존재하는 것입니다.
그럼 위의 예제 코드를 가지고 호이스팅에 대해 살펴보겠습니다.
console.log( a );
if (true) { // if 문에 의한 블록 스코프가 생긴다.
let a = 10; // 블록 스코프는 let 과 const 에 대해서만 영향을 받으므로 let 을 선언함. 10을 할당
if (true) { // if 문을 다시 만나서 또 다른 해당 블록 스코프가 생긴다.
console.log( a ); // ???
const a = 20;
}
console.log( a );
}
console.log( a );
첫 번째 console.log( a );
를 만났을 경우 호이스팅이 된다, 안된다를 가정해 봅시다.
- 호이스팅이 된다면...
const a
를 끌어올릴 것이고 자동으로undefined
가 들어갈 것으로 예상할 수 있습니다. - 호이스팅이 안된다면...
호이스팅이 이루어지지 않는다면 네 번째줄에서는 a 의 존재자체를 모를 것이고 자신의 스코프에 없으면 스코프 체이닝을 통해 상위 스코프를 찾을 것이고 그러면
a
는 10 이 출력될 것이라 예상할 수 있습니다.
위 두 가지 경우를 생각해 보고나서 실제 결과를 확인해 보면 a is not defined
란 에러가 나고 있습니다.
이러한 현상을 TDZ(Temporal Dead Zone) 라고 하는데 의역하자면 임시 사망지역, 임시 사각지대로 해석할 수 있습니다.
일반적으로 임시 사각지대라고 많이 불리고 있습니다. 이 TDZ 란 용어는 EcmaScript 에서 정의한 개념은 아니고,
다만 자바스크립트 개발자들이 이 개념을 가지고 언급하고 있습니다.
명세서에서는 이 지역에서 reference error 가 발생해야 한다라고 명세를 해 놓았는데 그 명세에 지칭할 용어를 만들어 놓지를 않아서
개발자들이 이러한 현상에 대한 것을 TDZ 라고 부르자라고 만들어낸 명칭입니다.
즉,정식 명칭은 아니지만 널리 통용되는 명칭이라고 생각하면 됩니다.
TDZ 라는 것은 let
이나 const
에 대해서 실제로 변수를 선언한 위치에 오기 전 까지는 해당 변수를 호출할 수 없다라는 것입니다.
자바스크립트에서 항상 주의하라고 했던 사항 중에 하나가 호이스팅입니다.
console.log( a );
// ... 많은 코드가 생략되어 있다고 했을 때 ...
var a = 10;
개발자가 일반적 상식으로 위부터 아래로 코드를 해석하는 순서에 따라야 하는데 소스 코드가 상당히 길었을 때 실수로 밑에서 선언한 변수를 상단에서 사용하려고 했을 때
어떠한 오류도 내지 않고 넘어가는 경우가 발생합니다. 개발자 입장에서는 에러를 나지 않는 것이 좋은 것이 아니라 에러가 나서 찾아서 고칠 수 있어야 좋은 것인데 잘못된 코드에 대한
에러를 내지 않으니 고치지 않을 확률이 높아지게 됩니다. 즉, 해당 오류를 찾기가 힘든 것입니다.
그래서 흔히 자바스크립트의 디버깅이 어렵다고들 말합니다. 그리고 대표적으로 함수 선언문도 비슷한 맥락입니다.
자바스크립트를 잘 모르는 사람이라면 이러한 호이스팅은 일반적 상식에서 벗어나 있기 때문에 자바스크립트의 암묵적인 룰을 알아야만 얻을 수 있는 사항입니다.
이러한 일반적 상식에서 벗어난 것들을 배제하기 위해서 나온 것이 ES6 이며,
ES6 의 철학은 암묵적인 규칙 그리고 예상하지 못했던 것들을 암기해야 숙지할 수 있었던 사항들을 최대한 배제하기 위한 것을 철학으로 삼고 있습니다.
하지만 지금까지 ES5 의 환경적인 룰들은 그대로 가져갈 수 밖에 없다라는 철학도 결부되어 있는 부분도 있습니다.
TDZ 를 설명하기 위해서는 기존 호이스팅에 대한 개념을 재정립할 필요가 있습니다.
기존 var :
- 변수명만 위로 끌어 올린다.
- undefined 를 할당(?)한다.(할당이라는 말보다는 값이 없을 경우 undefined 가 된다라고 설계되어 있는 표현이 정확함)
let, const :
- 변수명만 위로 끌어올리기만 한다. 이후 아무런 동작도 하지 않음
즉, 어떤 값과도 매칭되어 있지 않음을 의미합니다.
- 변수명만 위로 끌어올리기만 한다. 이후 아무런 동작도 하지 않음
위 예제 코드의 블록 스코프에서 let
, const
를 선언한 경우에 reference Error : a is not defined
가 출력된다는 것은
a
에 대한 존재를 알고 있는 것입니다.
만약 호이스팅이 되지 않는다면 스코프 체이닝을 통해 상위 스코프의 값을 찾아서 출력했을 것이기 때문입니다.
하지만 블록 스코프의 실행 컨텍스트가 열리는 순간 a 를 호이스팅하고 a 의 존재를 인지하고 있으나 다만 undefined
를 할당(?)하는 과정이 빠진 것입니다.
ES6 의 블록 스코프에서 호이스팅을 하지 않는다라고 소개하는 서적이 많은데 감히 말씀드리는데 호이스팅은 합니다.
다만, TDZ 라는 것이 있어서 위와 같은 현상이 나타나는 것입니다.
사실 공식 문서에서는 호이스팅을 하지 않는다라고 설명해 놓은 것이 없음에도 잘못된 유추로 호이스팅을 하지 않는다라고 잘못된 지식을 전달하고 있고
위의 현상만 놓고 보더라도 호이스팅을 안하는 것이 아니라 실질적으로 끌어올리기만을 한다는 것은 명백한 사실입니다.
즉, 끌어올리기는 하지만 할당(?)을 하지 않는다로 이해하면 될 것입니다.
모든 문 형태에 적용
위에서 살펴본 내용을 토대로 다음의 코드 결과값들을 예측해 보기 바랍니다.
{
let a = 2;
if (a > 1) {
let b = a * 3;
console.log( b );
} else {
let b = a / 3;
console.log( b );
}
console.log( b );
}
console.log( a );
let a = Math.ceil(Math.random() * 3); // 0 ~ 2.999...
switch (a) {
case 1 : {
let b = 10;
console.log( a + b );
break;
}
case 2 : {
let b = 20;
console.log( a + b );
}
case 3 : {
let b = 30;
console.log( a + b );
}
}
console.log( a, b );
지금까지 잘 따라왔다면 어렵지 않게 위 코드 결과값을 예측해 봤을 것입니다. 그리고 다음의 for 문
을 살펴보도록 합니다.
var sum = 0;
for (let i = 1; i <= 10; i++) {
sum += i;
}
console.log( sum );
console.log( i );
코드를 살펴보면 for 문
의 블록 스코프 밖에 let
이 선언되어 있는데 콘솔값의 i
는 어떻게 될까요?
for 문
의 조건식 부분은 블록 스코프에 포함하게 되어 반복문 밖에서 i 를 출력하려고 하면 reference Error 가 나게 됩니다.
사실 for 문
의 조건식은 인덱싱을 순차적으로 증가시키기 위한 목적만 가지고 있을 뿐이기 때문입니다.
let, const
위 내용에서 블록 스코프를 살펴보면서 let
과 const
를 다루어 보아서 충분히 인지했겠지만 다시 한번 상기면서 코드를 예측해보고
몇 가지 사항에 대해 추가적으로 살펴보도록 하겠습니다.
let
다음의 코드의 콘솔 결과를 예측해 보기 바랍니다.
let a = 1;
function fn() {
console.log( a, b, c );
let b = 2;
console.log( a, b, c );
if (true) {
let c = 3;
console.log( a, b, c );
}
console.log( a, b, c );
}
fn();
for (let i = 0; i < 5; i++) {
console.log( i );
}
console.log( i );
재할당 가능
let
은 var
와 똑같은 개념으로 이해하되 블록 스코프에 갇히며 TDZ 가 있다는 것을 숙지하고 있으면 됩니다.
let a = 1;
a = 2;
console.log( a );
반복문 내에서의 함수를 실행한 경우
반복문이 사용된 다음의 코드를 살펴봅니다.
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push(function() {
console.log( i );
});
}
funcs.forEach( function (fn) {
fn();
});
위 코드는 먼저 빈 배열을 선언한 후 빈 배열에 함수를 넣는 코드입니다. 그리고 나서 forEach
를 통해서 각각의 함수를 실행하도록 하는 코드입니다.
코드를 좀더 살펴보자면 i
가 순회할 때마다 빈 배열에 익명함수를 집어 넣고 있습니다. 즉, 다음과 같은 결과를 예상할 수 있습니다.
[
function() { console.log(i); },
function() { console.log(i); },
function() { console.log(i); },
function() { console.log(i); },
...
]
여기서 forEach
를 이용하여 함수를 실행하게 되면 i
의 값은 어떻게 될까요?
forEach 의 함수 실행은 반복문이 모두 순회한 후에 함수를 실행하게 됩니다. 실행 컨텍스트 함수를 실행할 때 열리게 됩니다.
즉, 함수를 실행할 때 비로소 변수를 호이스팅하고 this
를 바인딩하고 자신한테 없는 변수를 찾는 작업등을 하게 될 것입니다.
그럼 위 코드는 10번의 순회를 하면서 함수 10개가 생성되었을 것이고 그 다음 마지막 순회를 하고나서 i++
로 인해 for 문이 실행 종료된 후의 i
값은 10 이 되어있는 상태일 것입니다.
그렇기 때문에 함수를 반복문으로 계속 실행해도 i
는 계속 10 만 출력될 것입니다.
아마도 사용자는 일반적으로 0 ~ 9 가 차례대로 출력되는 것으로 예상했을 수 있습니다.
그렇다면 0 ~ 9 까지 차례대로 출력하게 하려면 어떻게 해야 될까요 ?
예상한 결과대로 출력하려면 i
의 유효범위인 i
가 계속 살아있도록 만들어줘야 합니다.
즉, 각각의 반복문 안에서 func.push(function() { console.log(i); })
에 i
를 넘겨(전달해) 줘야 할 것입니다.
i
값을 증가한 것을 나중에 읽어오는게 아니라 처음부터 미리 넘겨줘야 합니다.
그렇게 하기 위해서는 다음과 같이 즉시 실행 함수를 사용하게 됩니다.
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push(function(val) {
return function () {
console.log( val );
};
}(i));
}
funcs.forEach( function (fn) {
fn();
});
위와 같이 즉시 실행 함수로 i
값을 미리 넘겨주고 나서 나중에 함수를 호출했을 때 i
값이 호출(출력)되도록 리턴을 하게 됩니다.
즉, i
값을 미리 가지고 있게 한 후 나중에 호출할 때서야 그 값을 리턴문의 함수에서 출력하도록 하는 것입니다.
사실 이 내용을 다루기 위해서는 클로저(closure)라는 개념을 알아야 하지만 ES6에서는 블록 스코프가 생겨나면서 이러한 현상에 대해 고민할 필요가 없어지게 됩니다. 클로저에 대해서는 이후에 다시 자세히 다루면서 알아보도록 하겠습니다.
위 코드의 즉시 실행함수를 걷어내고 var
키워드를 let
으로 변경한 후 코드를 실행해 보도록 하겠습니다.
let funcs = [];
for (let i = 0; i < 10; i++) {
funcs.push(function() {
console.log( i );
});
}
funcs.forEach( function (fn) {
fn();
});
반복문 자체가 블록 스코프이기 때문에 각각의 i
값마다 스코프가 생성되므로 i
는 그대로 순차적으로 0 ~ 9 까지 출력되게 될 것입니다.
그래서 스코프로 인해 즉시 실행 함수를 사용하는 방법과 같은 고민들이 해소가 되어 메모리 소모를 덜 할 수 있게 되는 환경이 열리게 된 것입니다.
const
const
는 constant variable 의 약자입니다.
사전적 의미는 상수 변수라고 하는데 뜻이 형용 모순되는 듯이 보이지만 프로그래밍 언어에서는 무엇인가를 선언(지정)한 순간부터가 상수라고 해석될 수 있습니다.
그래서 아래와 같은 변수 A 에 10 을 할당하는 과정이 끝나야지만 A 가 상수로써의 역할을 하게 됩니다.
const A = 10;
재할당
상수는 아래와 같이 할당을 한 후에 재할당을 하게 되면 Assigment to constant variable 인 TypeError 가 발생하게 됩니다.
const A = 10;
A = 20;
최초 선언시 할당하라 !
위에서 설명했듯이 상수를 선언과 동시에 할당을 해야만 합니다. 즉, 문장이 끝나기 전에 할당해 줘야 하는 것입니다.
다음과 같이 선언 후에 재할당을 하게 되면 할당되지 않습니다.
const A;
const A;
라고만 선언하고 할당하지 않으면 Missing initializer in const declaration 이란 문법 오류가 발생합니다.
그리고 다음과 같이 다음 행에서 할당하더라도 오류가 나타납니다.
const A;
A = 10;
즉, const
는 선언과 동시에 값을 할당해 주어야합니다.
참조타입 데이터의 경우
const OBJ = {
prop1 : 1,
prop2 : 2
};
OBJ = 10;
OBJ 자체에 접근하므로 오류가 발생합니다.
const OBJ = {
prop1 : 1,
prop2 : 2
};
OBJ.prop1 = 3;
console.log( OBJ );
위 코드는 OBJ
자체에 접근한 것이 아니라 OBJ
란 변수와는 별개의 다른 주소 공간에 저장되어 있기 때문에 OBJ
가 가리키고 있는 따로 저장되어 있는 객체에 접근하여 객체 안에 있는
prop1
에 접근하라는 의미입니다.
즉, 상수는 OBJ 이고 객체 자체는 상수가 아니라는 뜻입니다.
- 12번 : OBJ => OBJ 야... 너는 20번을 보고 있거라.. 그리고 너는 이제 상수이므로 20번이 아닌 다른 주소를 할당할 수 없느니라 !!!
- 20번 : { }
=> { } 이 객체는 OBJ const 에 넣었기 때문에 이 객체 리터럴 자체를 바꿀 수 있는 방법은 없지만 객체 안에 있는 것은 상수가 아니기 때문에 얼마든지 변경할 수 있는 것입니다.
다음의 코드를 예측해 보기 바랍니다.
let a = { prop1: 1};
const b = a;
b = 10;
a = 20;
console.log( b );
- @100 : a -> 객체를 200번에 넣음
- @200 : {prop1 : 1 }
- @101 : b -> @200번 객체를 바라봄
- @100 : a -> @1000
- @1000 : 20
참조형 데이터를 상수에 할당할 경우에는 참조형 데이터 내부에 있는 프로퍼티들은 상수가 아니라는 점을 기억하기 바랍니다.
다음은 배열을 const 에 할당했을 경우를 알아봅니다.
const ARR = [0, 1, 2];
ARR.push(3);
ARR = 10;
delete ARR[0];
let, const 공통 사항과 사용 전략
위에서 let
과 const
에 대한 특성을 살펴봤다면 이 둘 간의 공통 사항들에 대해 알아보도록 하겠습니다.
유효범위
{
let a = 10;
{
const b = 20;
console.log(b);
}
console.log(a);
console.log(b);
}
console.log(a);
재선언(재정의)
다음의 코드는 var
를 사용한 경우입니다.
var a = 0;
var a = 1;
console.log(a);
위 코드가 호이스팅을 하게 되면 다음과 같을 것입나다.
var a;
var a; // 중첩되었으므로 하나만 남을 것임
a = 0;
a = 1;
console.log(a);
위와는 다르게 let
과 const
를 사용한다면...
let b = 2;
let b = 3;
console.log(b); // 식별자가 이미 선언되았다는 메시지가 출력
const b = 2;
const b = 3;
console.log(b); // 식별자가 이미 선언되았다는 메시지가 출력
그렇다면 var
와 let
, const
를 함께 사용한다면...
var b = 10;
let b = 20;
console.log(b); // 식별자가 이미 선언되았다는 메시지가 출력
위 코드에서 확인했 듯이 var
와 let
, const
는 함께 사용하지 않는 것이 좋습니다.
ES6 를 사용하는 환경에서는 var
키워드를 사용하지 말 것을 권장하고 있습니다.
그렇다면 let 과 const 중 무엇을 사용하는가?
이미 학습했다시피 let
, const
는 목적이 다르지만 프론트엔드 환경에서는 전략적으로 const
를 사용하는 편이 좋습니다.
일반적으로 프론트엔드 환경에서는 주로 객체를 다루기 때문이기도 하고 객체를 다루지 않는 변수를 쓰더라도 이 변수의 값을 재할당하면서 사용하는 경우가 생각보다 많지 않기 때문입니다.
물론 변수의 값이 재할당이 필요한 경우라면 let
을 사용하면 됩니다.
let
: 값 자체의 변경이 필요한 예외적인 경우에 사용.
const
: 객체에 주로 사용.
전역 객체의 프로퍼티와 전역 변수
다음의 코드를 콘솔에서 확인해 보도록 하겠습니다.
var a = 10;
console.log( window.a ); // 10
console.log( a ); // 10
delete a;
console.log( window.a ); // ?
console.log( a ); // ?
delete window.a; // 로그 확인해 볼 것
var c = {
d : 1
};
delete c.d; // 로그 확인해 볼 것
window.e = 10;
delete window.e;
console.log( e );
var f = 10;
delete f; // 로그 확인해 볼 것
delete window.f // 로그 확인해 볼 것
전역 공간에서 생성한 변수는 전역 변수임과 동시에 전액 객체의 프로퍼티가 됩니다.
전역 변수를 삭제하든 전역 객체의 프로퍼티를 삭제하든 간에 삭제가 되어야 하는 것이 논리적이긴 하지만 이러한 동작이 자바스크립트가 프로그래밍 언어로써 천대받는 이유 중의 하나이기도 했습니다.
이러한 비논리적인 동작으로 인해 전역 변수의 선언을 최소화하고 전역 공간을 침범하지 않기 위해서 함수 스코프를 만든다던지 즉시 실행 함수를 사용한다던지 등의 다양한 디자인 패턴이 등장하게 된
이유이기도 합니다.
let
을 사용한다면...
let aa = 20;
console.log( window.aa );
console.log( aa );
delete aa; // 로그 확인해 볼 것
window.bb = 30;
delete bb; // 로그 확인해 볼 것
객체의 프로퍼티는 삭제할 수 있지만 그렇지 않은 것은 삭제할 수 없는 지극히 논리적인 형태로 변경되었습니다.
즉, 전역 객체의 공간과 전역 변수의 공간은 별개로써 동작하는 환경이 생겨났습니다.
for 문에서의 주의사항
반복문 내에서 const
를 사용시 특이한 점이 있는데 이에 대한 주의사항을 알아보겠습니다.
다음과 같은 객체가 있을 경우 for~in 문과 for 문을 사용해 본 코드입니다.
var obj = {
prop1: 1,
prop2: 1,
prop3: 1
};
for (const prop in obj) {
console.log( prop );
}
for (const i = 0; i < 5; i++) {
console.log( i );
}
위 코드를 확인해 보면 for~in 문은 에러없이 결과를 출력하지만 for 문에서는 0 을 출력하고 Assignment to constant variable.
란 타입에러가 발생합니다.
for 문에 대한 블록 스코프을 학습했을 때 for 문의 괄호({})도 블록 스코프의 영역이고 const
는 재할당이 되지 않는 상수라고 언급했습니다.
이 개념대로라면 for 문의 i
를 출력했을 경우 i
를 재할당하려고 한 것이기 때문에 에러가 발생하는 것은 당연하지만
for~in 문의 경우는 에러없이 출력된 것으로 의아해 할 수 있습니다.
예상해 보건대 위 코드에서 for~in 문은 다음과 같이 내부적으로 동작할 것으로 예측할 것입니다.
prop ⇒ prop1
prop ⇒ prop3
prop ⇒ prop2
prop = prop1;
prop = prop2;
prop = prop3;
const
는 재할당이 되지 않는 상수인데도 불구하고 for~in 문에서는 재할당이 가능한 것으로 보이는 것은 예외적으로 이런 동작이 가능하도록 만들어 놓은 것이기 때문에
이러한 특성을 암기(?)/숙지해야할 필요가 있습니다.
for~in 문에서 const
를 사용할 경우에는 내부적으로 다음과 같은 동작이 일어난다고 볼 수 있습니다.
var obj = {
prop1: 1,
prop2: 1,
prop3: 1
};
for (const prop in obj) {
console.log( prop );
}
// 내부적으로 다음과 같은 동작이 발생
{
let keys = Object.keys(obj); // 객체에 있는 키들을 가져와서 배열로 저장
for (let i =0; i < keys.length; i++) { // 배열을 순회
const prop = obj[keys[i]];
console.log( prop );
}
}
// 즉, 여기서는 i 번째마다 각각의 블록 스코프가 생성
{
const prop = obj[keys[0]];
console.log( prop );
}
{
const prop = obj[keys[1]];
console.log( prop );
}
{
const prop = obj[keys[2]];
console.log( prop );
}
블록 스코프와 this
다음의 코드를 살펴보도록 합니다.
var value = 0;
var obj = {
value: 1,
setValue: function () {
this.value = 2;
(function () {
this.value = 3; // this : window, setValue, obj 중 무엇일까?
})();
}
};
obj.setValue();
console.log( value );
console.log( obj.value );
메소드에서 this
를 호출할 경우에는 마침표(.)의 바로 앞에 그 대상이기 때문에 obj.value
는 2 가 출력됩니다.
그렇다면 즉시 실행 함수 내부에서의 this.value
의 this
는 무엇을 가리킬까요?
위 코드의 메소드 안에서 함수(즉시실행 함수)는 일반 함수로써 this
는 전역 객체인 window
를 가리킵니다.
즉, this
는 window
를 가리키므로 window.value
라고 할 수 있어 console.log( value ); 는 3이 출력될 것입니다.
보통 개발자는 setValue
내부(메소드)에서의 this
와 중첩(내부)된 함수 안에서의 this
도 같았으면 하는 것이 편하고 논리적일 것입니다.
하지만 메소드 안의 중첩된 함수에서의 this
는 window
를 가리키는 특성때문에 ES5 까지만 하더라도 아래와 같은 코드로 우회하여
this
를 같도록 설정해 주었습니다.
var value = 0;
var obj = {
value: 1,
setValue: function () {
this.value = 2;
var self = this;
(function () {
self.value = 3;
})();
}
};
obj.setValue();
console.log( value );
console.log( obj.value );
var value = 0;
var obj = {
value: 1,
setValue: function () {
this.value = 2;
var self = this;
(function () {
this.value = 3;
}).call(this);
}
};
obj.setValue();
console.log( value );
console.log( obj.value );
위 코드들은 this
우회하는 대표적인 방법들이지만 블록 스코프가 나타나면서 간결하게 변경되었습니다.
다음은 블록 스코프만 적용된 코드입니다.
var value = 0;
var obj = {
value: 1,
setValue: function () {
this.value = 2;
{
this.value = 3;
}
}
};
obj.setValue();
console.log( value );
console.log( obj.value );
이는 사용자가 스코프를 만들고 싶으면서 this
는 동일하게 사용하고 싶은 경우에 유용하게 활용할 수 있습니다.
var value = 0;
var obj = {
value: 1,
setValue: function () {
let a = 20;
this.value = 2;
{
let a = 10; // 내부에서만 사용하려고 하는 변수가 필요한 경우
this.value = 3; // this 는 계속 동일하게 사용
}
}
};
obj.setValue();
console.log( value );
console.log( obj.value );
사용자가 다른 변수를 사용하고 싶으면서 어떤 스코프가 필요로 할 경우에 즉, 내부에서만 사용하려고 하는 변수가 있을 때
그 내부에서의 this
는 계속해서 동일하게 사용하려고 하는 경우 등에 활용할 수 있을 것입니다.
함수 스코프는 this 바인딩으로 인해 중첩된 함수는 window
를 가리키지만 블록 스코프는 this 바인딩을 하지 않는 특성으로 인해 바로 상위의
this
를 사용할 수 있습니다.