공부 정리/You Don't Know Js

[YDKJ] Scope & Closures - What's the Scope?

경적필패. 2023. 5. 2. 16:42
반응형

요약

Chapter 1: What's the Scope?

  • JS에서 변수를 구성하고 관리하는 매커니즘을 설명하기 위해서는 스코프에 대한 이해가 필요하다.
  • 이 책에서는 scope가 작동하는 방식, 유용성, 피해야 할 함정을 살펴볼 것.

 

Compiled vs. Interpreted

  • 컴파일은 일반적으로 소스 코드가 한번에 변환되어 나중에 실행될 수 있는 파일로 저장됨.
  • 인터프리터는 컴파일러와 유사하게 프로그램을 기계가 이해할 수 있는 명령어로 변환함. 그러나 처리모델은 다르고 소스코드가 한 줄씩 변환됨.

 

Compiling Code

  • 스코프는 주로 컴파일 중에 결정 됨. 따라서 컴파일과 실행을 이해하는 것이 스코프와 큰 관련이 있다.
  • 컴파일러 이론에서 프로그램은 세가지 기본 단계로 처리됨.
  • 1. Tokenizing : 문자열을 의미 있는 청크인 토큰으로 나누는 것. var a = 2. => var, a,=,2
  • 2. 파싱 : 토큰배열을 가져와 프로그램의 문법 구조를 나타내는 트리구조로 변경한다.(Abstract Syntax Tree, AST)
  • 3. 코드생성 : AST를 가져와 실행 가능한 코드로 변환하는 것.

SCOPE의 범위에선 벗어나지만 JS는 다른 언어와 달리 빌드 단계에서 컴파일이 일어나지 않기 때문에 매우 제한된 시간 내에 컴파일과 최적화를 수행해야 합니다. 그러기 위해 lazy 컴파일, JIT 같은 트릭을 사용합니다.

 

 

Required: Two Phases

  • JS프로그램 처리는 적어도 두 단계로 구성됨. 구문분석/컴파일 이후에 실행.

 

Syntax Errors from the Start

var greeting = "Hello";

console.log(greeting);

greeting = ."Hi";
// SyntaxError: unexpected token .

 

"Hi"앞에 있는. 토큰으로 인해 syntax에러가 발생함.

JS가 한 줄씩 실행하는 거라면 "Hello"를 출력해야 하지만, 실상은 그렇지 않음.

실제로 JS엔진이 첫 번제와 두 번째 줄을 실행하기 전에 전체 프로그램을 먼저 구문 분석함.

 

 

Early Errors

console.log("Howdy");

saySomething("Hello","Hi");
// Uncaught SyntaxError: Duplicate parameter name not
// allowed in this context

function saySomething(greeting,greeting) {
    "use strict";
    console.log(greeting);
}

여기서도 마찬가지로 실행되기 전에 구문분석(parse)이 이루어지기 때문에 "Howdy"는 출력되지 않는다.

(greeting, greeting) 중복

 

 

Hoisting

function saySomething() {
    var greeting = "Hello";
    {
        greeting = "Howdy";  // error comes from here
        let greeting = "Hi";
        console.log(greeting);
    }
}

saySomething();
// ReferenceError: Cannot access 'greeting' before
// initialization

TDZ에 대해서는 5장에서 다룰 것임. => let이 선언만 되고, 초기화는 안 돼서 일어나는 현상

 

 

Compiler Speak

var students = [
    { id: 14, name: "Kyle" },
    { id: 73, name: "Suzy" },
    { id: 112, name: "Frank" },
    { id: 6, name: "Sarah" }
];

function getStudentName(studentID) {
    for (let student of students) {
        if (student.id == studentID) {
            return student.name;
        }
    }
}

var nextStudent = getStudentName(73);

console.log(nextStudent);
// Suzy
  • JS엔진이 프로그램의 변수를 올바르게 처리하려면, 각 변수를 target이나 source로 구별해야 한다.

 

Targets

target 예시

students = [ // ..

students가 target

 

for (let student of students) {

student가 target

 

getStudentName(73)

function getStudentName(studentID) {

studentId가 target임

그러나 함수선언은 target의 특수한 경우임.

var getStudentName = function(studentID)처럼 생각할 수 있지만. 정확하진 않음.

getStudentName과 =function(studentID)모두 컴파일 시에 처리됨. 둘 간의 연결은 스코프의 시작에 자동적으로 처리된다(=를 기다리지 않고)=> 자세한 것은 5장에.

 

Sources

for (let student of students) {

에서 students가 소스.

 

if (student.id == studentID)

에서 student, studentID 모두 소스

 

getStudentName(73)

에서 getStudentName이 소스

 

Cheating: Runtime Scope Modifications

  • 컴파일될 때 스코프가 결정됨.
  • 그러나 비엄격(non strict)에서는 런타임 때 스코프를 수정하는 방법이 존재함
  • 이는 혼란스럽지만, 알아두어야 함.

eval() 함수는 런타임에 컴파일함.

function badIdea() {
    eval("var oops = 'Ugh!';");
    console.log(oops);
}
badIdea();   // Ugh!

이처럼 런타임에 스코프가 결정됨.

 

var badIdea = { oops: "Ugh!" };

with (badIdea) {
    console.log(oops);   // Ugh!
}

with 키워드는 동적으로 로컬스코프를 만듦. 이 또한 런타임에 결정됨.

최대한 eval과 with을 안 쓰는 게 좋다고 저자는 추천함 => 엄격모드에서는 어차피 사용 불가.

 

Lexical Scope

  • 우리는 스코프가 컴파일단계에서 결정된다는 것을 증명함. 이러한 종류의 스코프를 렉시컬 스코프라 함.
  • let/const는 var과 달리 가장 가까운 블록과 연결됨.
  • 컴파일러는 런타임 때 사용할 수 있는 렉시컬 스코프 맵을 생성함.(컴파일이 스코프와 변수를 위해 메모리를 예약하지 않음)
  • 즉, 스코프는 컴파일 중에 식별되지만, 실제로 실행될 때마다 스코프가 생성됨.

 

 

 

느낀 점

스코프를 이렇게 깊게 생각해 본 적이 없었던 것 같다.

여태까지는 그냥 블록 안의 유효범위 정도라고만 생각했는데, 이 책을 통해 구문분석/컴파일 후 실행과정과 스코프의 관계를 알게 되었고, 스코프가 언제 식별되는지 언제 생성되는지 알게 되었다.

 

 

 

 

 

반응형