Chapter 1: What’s the Scope? - hochan222/Everything-in-JavaScript GitHub Wiki
JS Engine은 프로그램이 시작하기 전에 어떻게 동작할까..?
Compiled vs. Interpreted
Code compilation은 우리의 코드를 컴퓨터가 이해할 수 있는 instruction 목록으로 변환하는 일련의 단계이다.
Interpretation은 프로그램을 기계가 이해할 수있는 명령어로 변환한다는 점에서 컴파일과 유사한 작업을 수행하지만, 처리 모델이 다르다. 한 번에 컴파일되는 프로그램과는 달리 소스 코드는 한 줄 씩 변환된다. 각 행 또는 명령문은 소스 코드의 다음 행 처리를 즉시 진행하기 전에 실행된다.
현대의 JS 엔진은 실제로 JS 프로그램을 처리 할 때 compilation과 interpretation의 다양한 변형을 사용합니다.
Compiling Code
JS가 컴파일되는지 안되는지가 왜 중요할까?
=> Scope는 주로 컴파일 중에 결정되므로 compilation과 interpretation이 어떻게 관련되는지 이해하는 것이 중요하다.
고전적인 컴파일러 이론에서 프로그램은 다음과 같은 세 가지 기본 단계에서 컴파일러에 의해 처리된다.
- Tokenizing/Lexing
- 문자열을 token이라고 불리는 의미있는 덩어리로 나누는 작업이다. (예를 들어,
var a = 2;
는var, a, =, 2, ;
로 나뉜다. 공백은 token에서 의미있던 없건 간에 처리되지 않는다. (Tokenizing과 Lexing의 차이는 보통 미묘하고 학문적이지만, stateless와 stateful에 있다.)
- Parsing
- token의 stream (array)을 가져와서 프로그램의 문법적 구조로 나타내주는 트리를 만드는데 이것을 AST(추상 구문 트리)라고 한다. (
var a = 2;
에서 가장 상위 node는 VariableDeclaration (설명: var)으로 시작하고 그 하위 node는 Identifier (설명: a) 그 하위는 NumericLiteral(설명 2)를 갖는 AssignmentExpression(설명 =) 가있다.)
- Code Generation
- AST를 가져와서 실행 가능한 코드로 만들어 준다. JS 엔진은
var a = 2
에 대해 AST를 사용하고 이를 기계의 instructions로 변환하여 실제 변수를 생성하여 저장한다.(메모리 예약 포함)
A tokenizer breaks a stream of text into tokens, usually by looking for whitespace (tabs, spaces, new lines).
A lexer is basically a tokenizer, but it usually attaches extra context to the tokens -- this token is a number, that token is a string literal, this other token is an equality operator.
A parser takes the stream of tokens from the lexer and turns it into an abstract syntax tree representing the (usually) program represented by the original text.
JS 엔진은 이 세 단계보다 훨씬 더 복잡하다.구문 분석 및 코드 생성 과정에서 실행 성능을 최적화하는 단계가 있다.(중복된 요소를 축소한다던가...) 코드는 실행이 진행되는 동안 다시 컴파일되고 다시 최적화 될 수도 있다.
JS 엔진은 작업과 최적화를 수행 할 시간이 충분하지 않는데 JS 컴파일은 다른 언어와 마찬가지로 빌드 단계에서 미리 발생하지 않기 때문이다.
일반적으로 코드가 실행되기 직전에 마이크로 초 내에 발생해야한다. 이러한 제약 조건에서 가장 빠른 성능을 보장하기 위해 JS 엔진은 모든 종류의 트릭을 사용한다. (JIT 같이 컴파일을 지연시키는게 있다.)
Required: Two Phases
자바스크립트 프로그램 처리 과정을 두 단계로 나누면, 파싱과 컴파일이 먼저 일어나고 실행된다는 것이다. JS 명세는 compliation
가 명세돼있지는 않지만 본질적으로는 컴파일 후 실행된다. 이는 syntax errors, early errors와 hoisting으로 알 수 있다.
Syntax Errors from the Start
var greeting = "Hello";
console.log(greeting);
greeting = ."Hi";
// SyntaxError: unexpected token .
다음 코드가 있다. 이 프로그램은 출력을 만들지 않는다. 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);
}
"use strict"가 함수 saySomething 내에 없을 때에는 구문 오류가 아니지만, 있으면 초기 오류로 중복 파라미터 오류가 나야한다. 변수가 복제 됐다는 사실과 "use strict"의 정보는 함수 밑에 나오는데 어떻게 알 수 있을까? (인터프리터라면!) 코드가 실행되기전에 구문 분석이 완전히 된다.
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
greeting에 에러가나는데 이는 Hello가 아닌 hi에 속한다. 오류가 발생한줄 다음 부분에 선언으로 인해 오류가 발생함을 아는 방법은 이전 단계에서 처리한 경우 밖에 없다. 위 원인은 TDZ와 관련있다.
컴파일은 이진표현으로 변환하는 것을 뜻한다. 위 예시로 확실해졌다. 구문분석은 확실히 일어난다. 구문 분석 후 컴파일되고 AST로 파싱된다. JS를 컴파일 언어로 규정짓는 것은 바이너리 파일파일 유무보다 이런 일련의 과정에 있다.
Cheating: Runtime Scope Modifications
프로그램이 컴파일 될 때 범위가 설정된다.
그러나 "use strict"가 아닐 때는 런타임중에 수정 할 수 있는 두가지 트릭이 있다.
이 두 가지는 모두 사용되서는 안되지만 인식하고 있어야 한다.
- eval(..) 함수는 프로그램 런타임 동안 즉시 컴파일하고 실행할 코드 문자열을 받는다. 함수나 var 선언이 있을 경우 현재 범위를 수정한다. (최적화된 Scope를 수정, 성능저하 등 좋지 못하다)
function badIdea() {
eval("var oops = 'Ugh!';");
console.log(oops);
}
badIdea(); // Ugh!
- with 키워드로 객체를 local 범위로 동적 변환한다. 또한, 컴파일 시간이 아닌 런타임에 범위로 바뀌고, 속성은 새 범위의 블록에서 식별자로 처리된다.
var badIdea = { oops: "Ugh!" };
with (badIdea) {
console.log(oops); // Ugh!
}
결론, "use strict"를 사용하자.
Lexical Scope
lexical scope의 핵심 아이디어는 전적으로 함수, 블록 및 변수 선언의 배치에 의해 제어된다는 것이다.
함수 내부에서 block scope를 갖도록 변수를 정의하면(let이나 const), 가장 가까운 {}와 범위가 연결된다.
변수에 대한 참조는 사용 할 수 있는 lexical scope 내에서 와야하며 가능 할 때까지 계속 중첩을 제거하며 전역에 도달하고 다른 범위에 갈 수 없을 때까지 이 과정이 반복된다.
compilation은 실제로 스코프 및 변수에 대한 메모리 예약 측면에서 아무 작업도 수행하지 않는다. 대신 compilation은 런타임에 필요한 lexical scopes의 모든 정보(lexical environments)를 완성한다.