Skip to content

Commit

Permalink
vault backup: 2024-11-27 22:29:12
Browse files Browse the repository at this point in the history
  • Loading branch information
Seongil-Shin committed Nov 27, 2024
1 parent 21f19ec commit cdf6351
Showing 1 changed file with 107 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: You don't know JS Yet 2부 - 스코프
author: 신성일
date: 2024-11-26 19:00:00 +0900
categories: study, YDKJSY
tags:
- "#study"
---
JS에서 함수는 일급값이기에 변수에 할당할 수 있고, 다른 곳에 넘길수도 있다. 그런데 다른 곳으로 넘어간 함수 내에서 외부 변수를 사용하는 경우, 해당 변수는 어딘가에 접근해야하므로 JS 에서는 함수를 프로그램 내 어디에서 실행하든 상관없이 함수 정의시 결정된 스코프를 유지한다. 이를 **클로저**라고 한다

## 1.2 컴파일 vs 인터프리트

컴파일 : 텍스트 형식으로 작성한 코드를 처리해서 컴퓨터가 이해할 수 있는 작업 지시 목록으로 바꾸는 일련의 과정. 이 과정은 전체 소스코드에 걸쳐 한번만 발생한다.

인터프린트 : 컴파일처럼 프로그램을 기계가 해석할 수 있는 명령으로 바꾸나, 컴파일과 달리 한줄씩 변환된다.

JS는 인터프린트 언어로 생각하는 경우가 많고 인터프린트를 거치지만, 컴파일 과정 또한 거친다. 이는 JS 실행 전 필요한 한 과정을 담당한다. 따라서 인터프리터 언어이기도 하지만 컴파일 언어로도 볼 수 있다.

## 1.3 코드 컴파일

스코프는 주로 컴파일 중에 결정된다. 따라서 스코프를 정복하려면 컴파일과 실행이 어떻게 연관되는지 이해해야한다.

고전 컴파일러 이론에서는 컴파일러는 주요 세 단계를 거친다고 정의한다
1. 토크나이징 / 렉싱
2. 파싱 : 토큰 배열을 문법 구조를 반영하는 AST로 바꾼다
3. 코드 생성 : AST를 컴퓨터가 실행 가능한 코드로 변환한다.

JS엔진은 더 복잡하게 돌아간다. 파싱과 코드 생성 단계에서 최적화를 위해 몇가지 추가작업을 하고, 프로그램 실행 중 컴파일이나 최적화를 다시 할 수도 있다.

JS 컴파일은 실행 전에 일어나기에 매우 빠르게 완료되어야한다. 따라서 JS엔진은 레이지 컴파일, 핫 리컴파일같은 방법을 사용한다.

### 필수 두 단계

JS 에서는 파싱과 컴파일이 먼저 일어난 후 실행이 되어야한다. 그렇지 않으면 ECMA 명세서를 지킬 수가 없게 된다.

만약 컴파일을 먼저하지 않으면 명세서에서 명시된 구문 오류, 초기 오류, 호이스팅을 대응할 수 없다.
- 구문 오류 : JS는 구문에 오류가 있을때 실행 전에 "SyntaxError"를 발생시킴. 이는 파싱이 실행 전 먼저 일어난다는 뜻
- 초기 오류 : ECMA 명세서에서는 엄격 모드에서 프로그램을 실행할 때, 가이드를 어긴 경우 초기 오류를 낸다. 이또한 파싱이 먼저 일어난다는 것을 알 수 있다
- 호이스팅 : let, const로 선언한 변수를 너무 빨리 접근하면 에러가 발생하며 실행되지 않는데, 이또한 파싱의 증거다

물론 파싱이 일어나고 컴파일이 일어나지 않을 수는 있다. 하지만 JS 엔진이 파싱을 통해 AST를 만들어놓고, 이를 효율적인 바이너리 코드로 변경하는 컴파일을 하지 않는다는 것은 생각하기 어렵다.

(저자에 따르면) JS는 일반적인 컴파일 언어와 달리 배포시 컴파일이 필요한 것은 아니지만 실행 전 컴파일이 필요한 것이라고 생각하면 된다


## 1.4 컴파일러체

선언을 제외하고 모든 변수와 식별자는 할당의 타깃이나 값의 소스 중 하나이다. 만약 할당된 값이 있다면 변수는 할당의 타깃일 것이고, 그렇지 않으면 변수는 값의 소스가 된다.

변수 처리를 위해 JS 엔진은 변수가 나타날때마다 변수 각각에 타깃과 소스라는 역할 이름을 붙인다.

### 할당의 타깃 사례들

```js
students = [] // 1. 단순할당

for (let student of students) {} // 2. 루프가 돌때마다 할당됨

getStudentName(73); // 3. 인자로 할당

function getStudentName(studentId) {} // 4. 스코프를 구성하는 시점에 할당
```

코드에서 식별자 getStudentName는 컴파일 타임에 선언되고, `= function(studentId)` 도 컴파일 과정에서 처리된다. getStudentName와 함수의 관계는 할당문이 실행될 때 설정되지 않고, 스코프가 구성되는 시점에 자동으로 설정된다. (함수 호이스팅)

### 값의 소스

```js
for (let student of students) {} // 1. students에서 값을 가져옴

getStudentName(73); // 2. getStudentName을 참조함

if(student.id == studentId) // 3. 양쪽 다 참조함

console.log(student) // 4. console, student 참조함
```

## 1.5 런타임에 스코프 변경하기

스코프는 프로그램이 컴파일 될 때 결정되지만, 런타임 환경에는 영향을 받지 않는다. 하지만 비엄격모드에서는 런타임에서 스코프를 수정할 수 있는 방법이 두가지 있고, 이 두가지는 사용하면 안된다. (두 방법 모두 엄격모드에서는 사용이 불가능하다)

1. eval() : eval 에 넘기는 소스코드에 var나 function 선언이 있을 경우 이 선언들은 evel()이 실행중인 스코프를 변경시킨다. eval에는 다른 문제도 있지만, 컴파일과 최적화가 끝난 스코프를 다시 수정하기에 성능 이슈도 있다.
```js
function bad() {
eval("var oops = '이런'");
console.log(oops);
}
bad() // 이런
```

2. with 키워드 : 특정 객체의 스코프를 지역 스코프로 동적으로 변환한다. 이 역시 가독성과 성능 측면에서 안좋으니 사용하지 않는 것이 좋다

```js
var bad = { oops : "이런" }

with (bad) {
console.log(oops); // 이런
}
```

## 1.6 렉시컬 스코프

이렇게 스코프가 컴파일 타임에 결정되면, 이 스코프를 렉시컬 스코프(어휘 스코프)라고 한다. 이때 렉시컬은 컴파일 과정 중 `렉싱`과 관계가 있다.

이 렉시컬 스코프는 함수나 블록, 변수 선언의 스코프는 전적으로 코드 배치에 따라 제한된다.

컴파일은 스코프와 변수의 메모리 예약 관점에서 실제로는 아무것도 하지 않는다. (스코프를 생성하지 않는다.) 대신 프로그램 실행에 필요한 모든 렉시컬 스코프가 들어간 지도가 만들어진다. 여기서 렉시컬 스코프가 정의되고, 각 스코프에 해당하는 식별자가 추가된다.

0 comments on commit cdf6351

Please sign in to comment.