호이스팅
,스코프
,클로저
등JavaScript
의 실행과정에 대한 마지막 개념으로 우리는컴파일레이션
에 대한 이해가 필요하다.
IR
(중간 언어)로의 컴파일
과정을 단순화 한다면, 소스코드를 파싱한 뒤 AST
를 생성하여 중간 언어인 바이트 코드
로 컴파일
하는 과정으로 볼 수 있다.
이때 우리가 알고자 하는 var를 쓰지 않는 이유와 밀접한 관련이 있는 부분은 바로 파싱과정이다.
파싱과정을 나눈다면 크게 아래와 같이 나눌 수 있다.
1. 토크나이징/렉싱
2. 파싱
그럼 해당 단계들에 대해 간단히 짚고 넘어가보자.
토크나이징/렉싱은
JavaScript Engine
이 소스 코드를 만났을 시 가장 먼저 진행하는 부분이다.
토크나이징
과 렉싱
의 차이점에 대해서는 간단하게 설명할 수 있다.
var a = 5;
여기서 var
, a
, =
, 5
, ;
로 나누는 것이 토크나이징
이라 할 수 있다.
즉 우리가 작성한 소스코드를 텍스트화 하여 가장 기본 단위로 잘개 쪼개는 과정이라 할 수 있다.
만약 해당 과정 중 상태 유지 파싱 규칙을 적용해 a
가 별개의 토큰인지, 다른 토큰의 일부인지 파악한다면 그것을 렉싱
이라 한다.
이 단계에서 어휘(렉시컬)단위를 변수, 예약어 등와 같이 분류하기도 하고 토큰들의 연관성을 분석하기도 한다.
이 과정 중 스코프가 정의된다면 그것을 렉시컬 스코프라고 한다.
파싱은 다시 구문 분석과 의미 해석 으로 나누어진다.
구문 분석
은 코드에서 구조를 분석하는 과정이다. 텍스트가 예상 형식을 따르는지 여부를 확인하여 작성한 소스 코드의 오류 유무를 체크한다.
또한 해당 언어의 규칙을 기반으로 토큰
을 가지고 `구문 트리
를 생성한다
의미 분석은 의미론적 일관성을 확인한다.
의미 분석은 구문 트리를 해석해서 필요 없는 부분을 삭제하기도 하고, 필요한 부분을 추가하기도 하여 AST
(추상 구문 트리)를 생성한다.
구문 분석이 단순 코드의 외형적인 해석이라면 의미 분석은
변수 선언과 참조를 연결
한다던지, 스코프를 구별
한다던지, 타입 불일치를 판별
한다던지, 선언되지 않은 변수를 확인
한다던지, 의미론적으로의 연결성, 일관성을 체크한다.
참조 : What is JavaScript AST, how to play with it? (stack overflow)
위에 완성된
AST
를 가지고네이티브 코드
, 혹은바이트 코드
로 변환하는 과정을컴파일
이라고 한다.
프로그래밍은 변수나 함수에 이름을 부여하여 의미를 갖도록 한다. 만약 이름이 없다면, 변수나 함수는 다만 그저 하나의 메모리 주소에 지나지 않는다.
초기 프로그래밍 언어는 이 대응표를 프로그램 전체에서 하나로 관리했는데, 이는 이름 충돌 등 다양한 문제를 발생시켰다. 그래서 충돌을 피하기 위해, 각 언어마다 "스코프"라는 규칙을 만들어 정의하였다. 그렇게 스코프 규칙은 언어의 명세(Specification)가 되었다.
즉 스코프는
참조 대상 식별자
(identifier)를 찾아내기 위한 규칙이다.
변수
, 함수
의 이름과 같이 우리가 선언한 식별자를 찾아내 참조 할 수 있는지 없는지를 판단하는 기준이며, 식별자는 스코프
를 통해 자신만의 유효한 범위를 갖는다.
var x = 'global';
function foo () {
var x = 'local';
console.log(x);
}
foo(); // 'local'
console.log(x); // 'global'
스코프는 크게 동작, 레벨을 기준으로 분류할 수 있다.
렉시컬 스코프(정적 스코프)는 식별자가 정의(선언)될 때 결정된다.
var x = 'static';
function foo(){
var x = 'local';
bar();
}
function bar(){
console.log(x); // "static"
}
foo(); // "static"
위 코드를 보면
foo()
는 x
를 local
로 바꾸고 bar()
를 호출한다.bar()
는 x
를 출력하지만, 정적 스코프 언어
에서 x
는 이미 전역 변수로 선언되었기 때문에 그대로 static
을 출력한다.동적 스코프는 식별자가 실행, 혹은 호출될 때 결정된다.
var x = 'dynamic';
function foo(){
var x = 'local';
bar();
}
function bar(){
console.log(x); // "local"
}
foo(); // "local"
위 코드를 보면
foo()
는 정적 스코프
일때와 마찬가지로 x
를 local
로 바꾸고 bar()
를 호출한다.bar()
는 x
를 출력하지만, 동적 스코프 언어
에서는 bar()
는 foo()
에 의해 호출되었으므로 foo()
가 가지고 있는 변수 x
의 값인 local
을 출력한다. 즉 x
를 전역 변수
로 선언 하던 말던 본인이 실행되거나 호출 되었을때의 스코프
를 유효범위로 가진다.참고로 JavaScript는 렉시컬 스코프를 가진 언어이며, 함수 레벨 스코프를 가졌지만, ES6부터는 블록 레벨 스코프도 가진다.
위에 동작
스코프
가, 정적인지, 동적인지에 따라 분류된다면,레벨 스코프
는 유효 범위에 의한 분류이다.
전역 스코프는 말그대로 지역 전체의 스코프이며 어디서든지 참조가 가능하다.
만약 하나의 html에서 다수의 js파일을 로드해서 사용하더라도 전역 스코프를 가진 변수는 사용이 가능하다. 이는 전역 변수는 전역 객체의 프로퍼티이기 때문인데 브라우저의 window처럼 전역 객체는 실행 컨텍스트에 컨트롤이 들어가기 전부터 기본으로 생성되기 때문이다.
var x = 'global';
function foo(){
var y = 'local';
console.log(x); // 'global'
console.log(y); // 'local'
}
foo();
console.log(x); // 'global'
console.log(y); // Uncaught ReferenceError: y is not defined
위 코드를 보면
x
는 어떠한 함수나 객체 안이 아닌 전역에 선언한 전역 변수다.y
는 foo()
안에 선언된 지역 변수다.foo()
안에서는 x
나 y
모두 정상적으로 출력되는 걸 볼 수 있는데, x
가 출력된 이유는 x
가 어디서든지 참조가 가능한 전역 스코프 변수이기 때문이다.y
는 정상적으로 출력되지 않는데, 이는 y
는 foo()
에서만 유효한 범위를 가지는 지역 스코프 변수이기 때문이다.지역 스코프는
함수 레벨 스코프
와블록 레벨 스코프
로 나누어 진다.
함수 레벨 스코프
는 함수를 유효범위로 가진다.
(var
로 선언한 변수, 함수들을 함수 레벨 스코프를 가진다. )
function foo(level){
if(level){
var x = level + ' scope';
}
console.log(x);
}
foo('function level') // "function level scope"
/*
var x 는 if(){}구문 안에 선언되었지만 function level scope이므로
function foo(){}안에서도 참조가 가능하다.
*/
블록 레벨 스코프
는 블록 { }을 유효범위로 가진다.
(let
,const
로 선언한 변수, 함수들을 함수 레벨 스코프를 가진다. )
function foo(level){
if(level){
let x = level + ' scope';
}
console.log(x);
}
foo('block level') // Uncaught ReferenceError: x is not defined
/*
let x 는 if(){}구문 안에 선언되었기 때문에 block level scope이므로 if{}을 벗어난
function foo(){}안에서 참조가 불가능하다.
*/