출처
You Don't Know JS (타입과 문법, 스코프와 클로저)
프로그래밍 언어의 기본 패러다임 중 하나는 변수에 값을 저장하고 저장된 값을 가져다 쓰고 수정하는 것이다.
변수를 프로그램에 추가하면...
특정 장소에 변수를 저장하고 나중에 그 변수를 찾으려면 잘 정의된 규칙이 필요하다.
이런 규칙을 스코프(scope)라 한다.
자바스크립트는 전통적으로 많은 컴파일러 언어처럼 코드를 미리 컴파일하거나 컴파일한 결과를 분산시스템에서 이용할 수 있는 것은 아니다.
하지만 자바스크립트는 전통적인 컴파일러 언어에서 컴파일러가 하는 일의 상당 부분을 세련된 방식으로 처리하기에 컴파일러 언어라 할 수 있다.
전통적인 컴파일러 언어의 처리 과정에서는 프로그램을 이루는 소스 코드가 실행되기 전에 보통 3단계를 거치는데 이를 컴파일레이션(compilation)이라 한다.
문자열을 나누어 토큰(token)이라는 조각으로 만드는 과정
빈칸은 하나의 토큰으로 남을 수도 있고 아닐 수도 있다. 빈칸이 의미가 있느냐 없느냐에 나뉨
토큰 배열을 프로그램의 문법 구조를 반영하여 중첩 원소를 갖는 트리 형태로 바꾸는 과정
파싱의 결과로 만들어진 트리를 AST(Abstract Syntax Tree)라 부른다. (추상구문트리)
var a=2; 의 AST
1. 변수 선언(Variable Declaration)이라 부르는 최상위 노드에서 시작
2. 최상위 노드는 'a'라는 값을 갖는 확인자(identifier)와 대입 수식(assignment expression)이라 부르는 자식 노드를 가짐
3. 대입 수식 노드는 '2'라는 값을 갖는 숫자 리터럴을 자식 노드로 가짐
AST를 컴퓨터에서 실행 코드로 바꾸는 과정
언어에 따라, 목표하는 플랫폼에 따라 크게 다름
자바스크립트 엔진은 위 3가지 단계 외에도 많은 부분에서 다른 프로그래밍 언어의 컴파일러보다 훨씬 복잡하다. 예컨대, 자바스크립트 엔진은 파싱과 코드 생성 과정에서 불필요한 요소를 삭제하는 과정을 거쳐 실행 시 성능을 최적화 시킴
자바스크립트 엔진이 기존 컴파일러와 다른 점은 자바스크립트 컴파일레이션을 미리 수행하지 않아서 최적화 할 시간이 많지 않다.
자바스크립트 컴파일레이션은 보통 코드가 실행되기 겨우 수백만분의 일초 전에 수행된다. 따라서 자바스크립트 엔진은 가능한 한 가장 빠른 성능을 위해 Lazy Compile, Hot Recompile 등의 트릭을 사용
정리하자면,
- 엔진
- 컴파일레이션의 모든 과정과 자바스크립트 프로그램 실행 담당
- 컴파일러
- 파싱과 코드 생성의 모든 잡일을 도맡음
- 스코프
- 선언된 모든 변수(확인자) 검색 목록을 작성하고 유지함
- 엄격한 규칙을 강제하여 현재 실행 코드에서 변수의 적용 방식을 정함
프로그램 var a=2; 를 보면 하나의 구문으로 보이나 엔진은 두 가지로 본다.
먼저 컴파일러를 보면, 컴파일러는
컴파일러가 다음 의사코드로 요약될 수 있는 코드를 생성한다고 생각할 수 있다.
변수를 위해 메모리를 할당하고 할당된 메모리를 a라 명명한 후 그 변수에 값 2를 넣는다.
그러나 실제 코드 생성 과정에서 컴파일러는 다소 다른 방식으로 프로그램을 처리한다.
엔진이 마침내 변수를 찾으면 값 2를 할당하고, 못 찾는다면 에러를 발생시킬 것이다.
정리하자면
별개의 2가지 동작을 취하여 변수 대입문을 처리
- 컴파일러가 변수를 선언 (현재 스코프에 미리 변수가 선언되지 않았으면)
- 엔진이 스코프에서 변수를 찾고, 변수가 있다면 값 할당
컴파일러가 스코프에서 변수에 대한 검색을 할 때 종류로 LHS와 RHS가 있다.
var a=2; // LHS
console.log(a); // RHS
function foo(a) {
console.log(a); // 2
}
foo(2);
스코프는 확인자 이름으로 변수를 찾기 위한 규칙의 집합
-> 그러나 보통 고려해야 할 스코프는 여러 개
하나의 블록이나 함수는 다른 블록이나 함수 안에 중첩될 수 있으므로 스코프 또한 다른 스코프 안에 중첩될 수 있다.
찾는 변수를 현재 스코프에서 발견하지 못하면 엔진은 상위 스코프로 넘어가는 식으로 변수를 찾고, 찾지 못한다면 최상위 스코프인 글로벌 스코프에 도달할 때까지 계속된다.
function foo(a){
console.log(a + b);
}
var b = 2;
foo(2); // 4
b에 대한 RHS 참조는 함수 foo 안에서 처리할 수 없고, 함수가 포함 된 스코프에서 처리함
중첩 스코프를 탐사할 때 사용하는 규칙
- 엔진은 현재 스코프에서 변수를 찾기 시작, 찾지 못하면 한 단계씩 올라감
- 최상위인 글로벌 스코프에 도달하면 변수를 찾든, 못찾든 검색을 중지
LHS와 RHS를 구분하는 이유는 두 종류의 검색 방식은 변수가 아직 선언되지 않았을 때
-> 즉, 모든 스코프에서 찾지 못했을 때 서로 다르게 동작하기 때문이다.
function foo(a) {
console.log(a + b); // RHS 실패
b = a;
}
foo(2);
// Uncaught ReferenceError: b is not defined
b에 대한 첫 RHS 검색이 실패하면 스코프에서 찾지 못한 변수이므로 선언되지 않은 변수가 된다.
선언되지 않은 변수를 사용하려 했기에 엔진은 ReferenceError를 발생시킨다.
function foo(a) {
b = a; // LHS 실패
console.log(a + b);
}
foo(2);
// 정상 작동
반면 엔진이 LHS 검색을 수행하고 실패하면, 글로벌 스코프 조차 엔진이 검색하던 변수가 없는 것이므로 글로벌 스코프는 엔진이 검색하는 이름의 새로운 변수를 생성해 엔진에게 넘겨줌
ES5부터 지원하는 'Strict Mode'는 글로벌 변수를 자동, 암시적으로 생성하지 못하게 하므로 이 모드가 적용되어 있다면 마찬가지로 ReferenceError를 발생시킨다.
이외에도 RHS 검색으로 변수를 찾았으나 그 값을 갖고 불가능한 일 하려 할 때 다음과 같은 에러 발생
ReferenceError는 스코프에서 대상을 찾았는가?
TypeError는 스코프 검색은 성공했으나 결과값을 갖고 적합한 일을 하는가?