본문 바로가기
공부 정리/You Don't Know Js

[YDKJ] Scope & Closures - Appendix A: Exploring Further

by 경적필패. 2023. 6. 13.
반응형

요약

이 부분은 본문에서 다룬 주제들에 대한 다양한 세부사항과 변칙적인 접근 방식을 탐구할 것.

이 부분은 다양한 주제와 장황한 내용을 포함하므로 여유롭게 읽을 것.

 

Implied Scopes

때때로 스코프는 명확하지 않은 위치에서 생성됨. => 이러한 스코프가 프로그램 작동에 큰 영향을 미치진 않지만, 그들을 알고 있는 것은 유용하다.

Parameter Scope

 

// outer/global scope: RED(1)

function getStudentName(studentID) {
    // function scope: BLUE(2)

    // ..
}

이 프로그램에서 스코프는 2가지가 됨.

 

// outer/global scope: RED(1)

function getStudentName(/*BLUE(2)*/ studentID = 0) {
    // function scope: GREEN(3)

    // ..
}

그러나 이 경우, 스코프는 3가지가 됨.

 

 

function whatsTheDealHere(id,defaultID = () => id) {
    var id;

    console.log(`local variable 'id': ${ id }`);
    console.log(
        `parameter 'id' (closure): ${ defaultID() }`
    );

    console.log("reassigning 'id' to 5");
    id = 5;

    console.log(`local variable 'id': ${ id }`);
    console.log(
        `parameter 'id' (closure): ${ defaultID() }`
    );
}

whatsTheDealHere(3);
// local variable 'id': 3   <--- Huh!? Weird!
// parameter 'id' (closure): 3
// reassigning 'id' to 5
// local variable 'id': 5
// parameter 'id' (closure): 3

첫 번째 콘솔메시지에서 왜 3이 나올까?

=> 이 특정한 상황에서 js는 undefined가 아니라 3으로 자동 초기화함.

=> 두개의 id는 하나의 변수처럼 보이지만 실제론 별개의 변수, 별개의 스코프임

id=5를 할당한 이후에는??

=> 매개변수 3은 유지되고, 로컬변수는 5로 변경

 

따라서,

1. 매개변수를 로컬변수로 가리지 말 것!

 

비단순 매개변수가 있다면 자체적인 범위를 갖는다는 사실만 알아도 조심할 수 있음.

 

Anonymous vs. Named Functions

Explicit or Inferred Names?

모든 함수에는 목적이 있음!

=> 목적이 없으면 제거해야 하는 함수임.

 

그럼 함수에 이름을 넣어야 할까?(익명 함수를 쓰지 말아야 할까?) => 저자는 YES

 

디버깅이 좋다

btn.addEventListener("click",function(){
    setTimeout(function(){
        ["a",42].map(function(v){
            console.log(v.toUpperCase());
        });
    },100);
});
// Uncaught TypeError: v.toUpperCase is not a function
//     at myProgram.js:4
//     at Array.map (<anonymous>)
//     at myProgram.js:3
btn.addEventListener("click",function onClick(){
    setTimeout(function waitAMoment(){
        ["a",42].map(function allUpper(v){
            console.log(v.toUpperCase());
        });
    },100);
});
// Uncaught TypeError: v.toUpperCase is not a function
//     at allUpper (myProgram.js:4)
//     at Array.map (<anonymous>)
//     at waitAMoment (myProgram.js:3)

아래 경우 어떤 함수에서 에러가 났는지 표기해 줌.

 

Missing Names?

그러나 추론된 함수의 경우에는 디버깅 스택에 표시해 줌

EX) const a = function(){}~~

그러나 이 경우 콜백에서는 추론된 이름을 사용할 수 없으므로 완전하지 않다.(대입 x)

 

Who am I?

이름이 없으면, 함수 자체적으로 참조할 수 있는 방법이 없음 => 재귀나, 이벤트로 사용할 때

물론 외부에 변수를 생성하여 제어할 수 있지만, 외부 범위에 의해 제어되므로 재할당 등 발생할 우려가 있음

 

Names are Descriptors

또한 함수에 이름이 없으면 읽는 사람이 빠르게 함수의 목적을 찾기 어려움

 

저자생각)

1. 함수에 이름을 안 넣은 건 게으름

2. 창의성결여(좋은 이름을 생각하지 못한)

절대로 예외는 없다!!!!!! 함수에는 이름이 있어야 한다.

 

Arrow Functions

화살표함수는 언제나 익명함수임.

화살표함수의 목적은 절대 타이핑을 절약하는 게 아니다. => 화살표 함수는 렉시컬 this를 가짐.

bind 같은 기법을 강제할 때를 해결하기 위해 특별히 설게 됐음.

따라서, 렉시컬 this가 필요한 경우 화살표함수를 사용해야 함.(상위의 this)

 

IIFE Variations

(function(){
    // don't do this!
})();

(function doThisInstead(){
    // ..
})();

IIFE에서도 익명함수는 사용할 수 없다.

 

!function thisIsAnIIFE(){
    // ..
}();

+function soIsThisOne(){
    // ..
}();

~function andThisOneToo(){
    // ..
}();

이런 식으로 IIFE를 정의할 수도 있다 => !,+,~는 표현식으로 바꿔주기 때문(근데 저는 한 번도 못 본 듯..)

 

Hoisting: Functions and Variables

호이스팅은 JS 설계상 실수라 자주 언급되지만, 왜 유용한지 살펴보자

 

  • 함수 선언을 맨 위로 올려줌으로써, 함수를 선언하기 전에도 호출할 수 있게 해 줌.
  • 변수 선언을 해당 스코프의 맨 위로 올려줌으로써 변수 선언 전에 참조할 수 있게 해 줌

 

Function Hoisting

getStudents();

// *************

function getStudents() {
    var whatever = doSomething();

    // other stuff

    return whatever;

    // *************

    function doSomething() {
        // ..
    }
}

이 코드처럼 함수 호이스팅을 사용한다면, 실행문을 가장 처음으로 만나며, 위에서 아래로 코드를 읽을 수 있기 때문에 가독성이 좋다(저자의견)

 

Variable Hoisting

let과 const도 호이스팅 되지만, 이 변수들은 TDZ 때문에 사용할 수 없음.

대부분의 경우 변수 호이스팅은 좋지 않다!!

pleaseDontDoThis = "bad idea";

// much later
var pleaseDontDoThis;

함수 호이스팅과는 달리 여기서는 코드를 이해하기 어렵게 만듦

 

The Case for var

Don't Throw Out var

var는 25년째 사용되었고 아주 잘 작동함, 설계가 잘못되었다는 것은 허풍임.

그렇다고 var가 모든 선언에 대한 올바른 선언자는 아님.

 

저자는 이렇게 생각한다.

블록 스코프 선언에서는 let을 선호한다. => TDZ는 실수지만 let자체는 훌륭함.

 

const-antly Confused

반면에 const는 그렇게 자주 사용하지 않는다.

const는 변경할 수 없는 값이 아니라, 재할당을 방지하는 것임.

const studentIDs = [ 14, 73, 112 ];

// later

studentIDs.push(6);   // whoa, wait... what!?

이처럼 변경은 가능함.(값을)

가변적인 값(배열, 객체)에 const를 사용하는 것은 함정을 만드는 것임. => 미래의 개발자가 걸리는

const는 숫자나, 문자열 변하지 않는 상수값을 써야 함.

또한 const를 쓰면 이후에 코드에서 재할당이 일어나지 않을 것이라는 정보를 줌!

이게 const의 모든 것.

(...나는 항상 객체에도 const를 사용해 왔다. 주소값이 변경되지 않는 것에 의미가 있다고 생각했는데 이는 좀 깊게 공부해봐야 할 듯하다)

 

var and let

저자는 const가 거의 유용하지 않다고 생각하므로 var과 let을 두고 생각함.

전역범위 사용을 최소화해야 하지만, 써야 한다면 var

물론 최상위범위에서도 let을 사용할 수 있지만, 그렇게 한다면 어떤 부분이 로컬화 된 건지 헷갈리게 할 것

 

저자는 블록스코프에선 무조건 let을 쓰라함. => 그것이 let이 만들어진 이유

function getStudents(data) {
    var studentRecords = [];

    for (let record of data.records) {
        let id = `student-${ record.id }`;
        studentRecords.push({
            id,
            record.name
        });
    }

    return studentRecords;
}

studentRecords는 함수 전체에서 사용되도록 의도되어있고 이를 알리기 좋음.

반면 record와 id는 좁은 범위에서만 사용되도록 의도되었음.

 

또한 var은 의도화 되지 않은 블록을 처리할 때 좋음. 대표적으로 try-catch

function getStudents() {
    try {
        // not really a block scope
        var records = fromCache("students");
    }
    catch (err) {
        // oops, fall back to a default
        var records = [];
    }
    // ..
}

try와 catch안에서 모두 var을 사용한 것은 어떠한 경로를 따라가더라도 records가 항상 선언된다는 것을 알리고 싶기 때문임.

이 때문에 우리는 의도치 않은 블록을 벗어나며, 함수 스코프에 여러 번 나타나는 것이 가능하다. => let으로는 불가함.

 

 

function getStudents() {
    var data = [];

    // do something with data
    // .. 50 more lines of code ..

    // purely an annotation to remind us
    var data;

    // use data again
    // ..
}

두 번째 var은 다시 선언하는 게 아니라, data가 함수 전체에 선언된 것임을 다시 알리는 것.

때문에 개발자는 50줄의 라인을 다시 찾아가서 읽을 필요가 없다.

(let도 빈 값으로 재할당 하면 되지 않나??)

 

저자는 var를 사용하라고 강조하고 있음 => 남들이 안 쓴다고 해서 안 쓰지 말고....

 

What's the Deal with TDZ?

TDZ를 왜 도입해야 했나??

 

Where It All Started

TDZ는 const에서 비롯된 것임.

TC39는 ES6 초기개발 도중 let과 const가 호이스팅 되지 않으면 쉐도잉과 같은 문제가 발생하리라 생각했음.

let greeting = "Hi!";

{
    // what should print here?
    console.log(greeting);

    // .. a bunch of lines of code ..

    // now shadowing the `greeting` variable
    let greeting = "Hello, friends!";

    // ..
}

js개발자에게는 hello, friends가 출력되는 게 더 직관적이었음. => 따라서 호이스팅 되어야 했다.

 

근데 왜 초기화는 시키지 않았을까?

이것은

{
    // what should print here?
    console.log(studentName);

    // later

    const studentName = "Frank";

    // ..
}

const는 호이스팅 되고 undefined로 초기화된 다음, 추후에 const studentName 문장에 도달한다면... 상수가 두 가지 값을 가지는 게 되어버림. => 따라서 초기화하지 않고(undefined값을 만들지 않고 studentName에 도달하면 값이 할당되게 함.

 

Who let the TDZ Out?

TC39는 const에 TDZ가 필요하기 때문에 let에도 TDZ가 필요할 것이라 판단.

 

 

 

느낀 점

매개변수에도 블록이 있는 건 처음 알았다! => 엄청 유용할지는 모르겠지만..!

저자가 익명함수를 싫어하는 이유를 공감하게 되었고, 화살표함수의 용도를 알게 되었다. => 어디 가서 코드가 짧아서 좋다고는 말 안 할 수 있게 되었다..!

 

여태 내가 알던 상식으로는 VAR은 사장되고, let과 const가 주류라고 알고 있었는데 저자는 var를 사랑하는 것 같다.

저자가 var을 사랑하는 이유는 주로 개발자가 코드를 읽을 때 도움을 준다는 관점이 많은 것 같다.

(전역변수에 let을 쓰면 어디가 로컬화된 건지 헷갈린다는 저자)

취향차이도 좀 있는 것 같다.

 

 

 

반응형

댓글