ECMAScript 6 - Scope

hogwarts·2021년 2월 8일
0

Angular Essentials

목록 보기
1/2

1. var, let

ES5에서 변수를 선언하는 유일한 방법은 var 키워드를 사용하는 것이다.
var 키워드는 다음과 같은 특징과 그로 인한 문제점을 갖는다.

  1. 함수 레벨 스코프 : for-loop의 초기화 식에서 사용한 변수를 for-loop 외부 전역에서 참조 가능하다.
  2. var 키워드 생략 허용 : 의도치 않은 변수의 전역화.
  3. 중복 선언 허용 : 의도하지 않은 변숫값 변경.
  4. 변수 호이스팅 : 변수를 선언하기 전에 참조 가능.

따라서 ES6에서는 이러한 var의 문제점을 해결하기 위해 let, const 키워드를 도입하였다.👍

2. Scope

사전적 의미는 '범위'다. 코드에서는 이 범위라는 것이 단지 공간적 의미만이 아닌 시간적 의미도 함께 부여된다. 즉, 시점(=순서)를 함께 포함하는 의미로 사용된다.

함수 : function() { }
블록 : { }

함수 레벨 스코프(var) : 함수 내부에서 선언한 변수는 지역변수, 그 외 블록 내부를 포함한 모든 변수는 전역변수다.

블록 레벨 스코프(let) : 코드 블록 내부에서 선언한 변수는 지역변수다.

함수 레벨 스코프

console.log(foo);   	// undefined

var foo = 123;		// 전역변수
console.log(foo);   	// 123

{
    var foo = 335;  	// 중복 선언이 허용된다. 그리고 함수 내부가 아니므로 전역변수다. 따라서 전역 변수 var foo = 123;의 값을 335로 바꾼다.
}

console.log(foo);   	// 335

블록 레벨 스코프

let foo = 123;		// 전역변수

{
    let foo = 324;      // 블록 레벨 스코프로 지역변수다.
    let bar = 231;
    console.log(foo);   // 324
}

console.log(foo);   	// 123
console.log(bar);   	// ReferenceError: bar is not defined

3. Hoisting

원래대로라면 변수는 참조를 하든 뭘 하든 반드시 호출하기 전에 선언이 되어 있어야 한다.

하지만 자바스크립트는 변수 선언을 최상단에 해둔 것처럼 동작하도록 하는 특성이 있는데 이것을 '호이스팅'이라 한다.🤔

자바스크립트에서 호이스팅은 모든 선언(var, let, const, function, function*, class)에서 이루어진다.

var로 선언한 경우 실제 선언문의 위치 이전에 호이스팅 된 변수를 호출해도 에러가 발생하지 않는다.

하지만 let으로 선언한 경우 실제 선언문 위치 이전에 호이스팅 된 변수를 호출할 경우 에러가 발생한다.

그 이유가 뭘까? 우선 변수 생성이 어떻게 이루어 지는지를 이해해야 var와 let의 차이를 이해할 수 있다.

변수 생성의 3단계

  1. Declaration phase(선언 단계) : 변수를 실행 컨텐스트의 변수 객체(Variable Object)에 등록한다. 이 변수 객체가 스코프가 참조하는 대상이다.
  2. Initialization phase(초기화 단계) : Instantiate하는 것이라 생각하면 된다. 메모리에 할당되며, 'undefined'로 초기화된다.
  3. Assignment phase(할당 단계) : Instance에 실제 값을 넣는다.

우선, var로 선언한 경우 1(선언 단계)와 2(초기화 단계)가 한 번에 이루어진다. 이후 3(할당 단계)를 거치게 된다.

console.log(foo);   // undefined
var foo;
console.log(foo);   // undefined
foo = 23;
console.log(foo);   // 23

let으로 선언한 경우 1(선언 단계), 2(초기화 단계), 3(할당 단계)가 모두 각각 이루어진다.

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

var, let 모두 작성한 소스코드 최상단에 호이스팅 된 변수 foo가 존재한다. 하지만 var는 호이스팅에 의해 선언됨과 동시에 초기화가 이루어지기 때문에 이미 인스턴스화 되어 'undefined'를 반환하고, let은 1(선언 단계)만 이루어진 상태이므로 아직 인스턴스화 되지 못 해 에러를 발생하게 되는 것이다. 따라서 인스턴스가 된 이후에서야 비로소 'undefined'를 출력하는 것이다.

이렇게 1(선언 단계)와 2(초기화 단계) 사이에서 변수를 호출할 경우 에러가 발생하는 이 스코프를 'TDZ(Temproal Dead Zone, 일시적 사각지대)'라고 한다.

따라서 다음과 같은 현상이 발생하게된다.

let foo = 90;    // 전역변수
{
    console.log(foo);   // 90, 전역변수 foo를 참조한다.
}
------------------------------------------------------
let foo = 90;    // 전역변수
{
    console.log(foo);   // ReferenceError: Cannot access 'foo' before initialization
    let foo = 23;	// 지역변수
}

두 번째 코드의 경우 let은 _'블록 레벨 스코프'_이므로 지역변수 foo에 의해 블록 최상단에 지역변수 foo가 호이스팅되고, 그 foo가 인스턴스가 되기 전에 호출하였기 때문에 에러가 발생하는 것이다.👀

4. Closure

var arr = [];

for (var i = 0; i < 3; i++) {
    arr.push(function () { console.log(i); });
}

// 배열에서 함수를 꺼내어 호출한다.
for (var j = 0; j < 3; j++) { arr[j](); }	// 3 3 3

arr[]에 저장된 것은 값이 아니라 함수다. 즉,

arr = [function () { console.log(i); },
       function () { console.log(i); },
       function () { console.log(i); }]

이고, i는 전역변수이므로 현재 i = 3인 상태다. 따라서 3을 3번 출력하는 것이다.

0, 1, 2를 출력하려면 다음과 같이 코드를 작성해야한다.

var arr = [];

for (var i = 0; i < 3; i++) {
    (function (k) {
        arr.push(function () { console.log(k); });
    }(i));
}

for (var j = 0; j < 3; j++) { arr[j](); }	// 0 1 2

여기서 arr[]은 다음과 같다.

arr = [function () { console.log(0); },
       function () { console.log(1); },
       function () { console.log(2); }]

클로저에 대해 좀 더 자세히 알 수 있도록 다른 예를 하나 더 첨부한다.

클로저는 함수 안에 값 자체를 저장한다.

function startAt(x){
    function incrementBy(y){
        return x + y
    }
    return incrementBy
}

var closure1 = startAt(1)   // 즉, function increbentBy(y) {1 + y}
var closure2 = startAt(2)   // 즉, function increbentBy(y) {2 + y}

console.log(closure1(3));   // 4 (1 + 3)
console.log(closure2(3));   // 5 (2 + 3)

이런식으로 전역변수를 함수 내에 지역변수처럼 고립시켜, 함수가 호출된 때의 값을 저장하게 만든다.😁

뭔가 코드가 한 눈에 들어오지 않는다. 위에서 0, 1, 2를 좀 더 쉽게 출력할 방법은 없을까?
바로 for-loop 안에 변수 선언에 let을 사용하면 된다🐶

var arr = [];

for (let i = 0; i < 3; i++) {
    arr.push(function () { console.log(i); });
}

// 배열에서 함수를 꺼내어 호출한다.
for (var j = 0; j < 3; j++) { arr[j](); }	// 0 1 2

let은 블록 레벨 스코프이기 때문에 fot-loop 블록 내에서만 변수로 작동한다. 즉, arr.push()에 의해 for-loop 밖의 arr에 founction () { console.log(i); }를 넣을 때는 변수 i가 아닌 이 함수가 생성된 때의 값 자체를 저장한다.

5. Global Object

자바에서는 모든 객체의 부모 클래스인 최상위 객체로써 Object 하나만 생각했었는데 자바스크립트에서는 두 가지를 이야기 한다. 1 ) 브라우저 사이드(Safari, Chrome, etc...)에서는 window 객체, 서버 사이드(Node.js)에서는 global 객체를 의미한다. var는 함수 내부가 아니면 모두 전역 변수라 했으므로 다음과 같은 접근이 가능하다.

var foo = 12;			// 전역 변수
console.log(foo);		// 12
console.log(window.foo);	// 12

let bar = 15;			// 지역 변수
console.log(bar);		// undefined
console.log(window.bar);	// 15

let을 전역 변수로 선언하고 싶다면??

let bar = 15;
window.bar = bar;
console.log(window.bar);	// 15

또 다른 방법 export, require를 사용한 전역 변수 사용

전역 변수는 남발하면 위험하기 때문에 전역에 걸쳐 고유한 값을 가져야 하는 경우가 아니라면 절대 사용하지 말자...

6. const

let은 블록 레벨 스코프를 같는 변수라면 const는 블록 레벨 스코프를 갖는 상수다. 하지만 const는 let과 다른 몇 가지 특징이 있다.

  1. 선언, 초기화, 할당 3단계가 한 번에 이루어져야 한다.⭐️
let foo;
foo = 15;
console.log(foo);

const bar;		// SyntaxError: Missing initializer in const declaration
bar = 20;
console.log(bar);

const zoo = 20;
console.log(zoo);	// 20
  1. 재할당이 불가능하다.
let foo = 15;
console.log(foo);   // 15
foo = 20;
console.log(foo);   // 20

const bar = 11;
console.log(bar);   // 11
bar = 17;           // TypeError: Assignment to constant variable.
  1. 객체 자체의 재할당은 금지되지만 객체의 property는 접근 가능하다.
const rainbow = { first: `led`
                , second: `orange`
                , thrid: `yello`
                , fourth: `green` };

console.log(rainbow);	// { first: 'led', second: 'orange', thrid: 'yello', fourth: 'green' }

// 객체의 재할당은 금지된다.
// rainbow = null; // TypeError: Assignment to constant variable.

// property는 접근 가능하다.
// 수정
rainbow.first = `purple`;

// 추가
rainbow.fifth = `blue`;
rainbow['sixth'] = `navy`;

// 삭제
delete rainbow.thrid;

console.log(rainbow);	// { first: 'purple', second: 'orange', fourth: 'green', fifth: 'blue', sixth: 'navy' }
  1. 객체의 property가 변경되더라도 객체 타입 변수에 할당된 주소값은 변경되지 않는다. 따라서 객체 타입 변수 선언에는 const를 사용하는 것이 좋다.
  1. 코드 가독성을 위한 상수 선언에 유용하다.

사실 5번은 const가 가지는 특징이라기 보다는 const가 가지는 2번째 특성(재할당 불가)을 이용해 가독성을 높이는 코드 작성 팁이다.

for (let i = 0; i < 3; i ++) {
    console.log(i);     // 0, 1, 2
}

const MAXROWS = 3;
for (let i = 0; i < MAXROWS; i ++) {
    console.log(i);     // 0, 1, 2
}




Reference
1. 이웅모, [Angular Essentials], Ruby paper, Chapter 3
2. "클로저 (컴퓨터 프로그래밍)", 위키백과, last modified Feb 26. 2020, accessed Feb 8. 2021, https://ko.wikipedia.org/wiki/클로저_(컴퓨터_프로그래밍)

profile
모르는 코드 10줄 보단 아는 코드 1줄

0개의 댓글