렉시컬 스코프(Lexical Scope): "소스 코드의 물리적 배치(정의 위치)에 의해 스코프 트리가 컴파일 시점에 만들어지고, 실행 중 식별자 해석은 그 트리를 '정의 시점' 기준으로 거슬러 올란간다."
var a = 2;
=> var
, a
, =
, 2
로 분리됨소스 코드 └─ 토큰화(토크나이징/렉싱) └─ 구문 분석(파싱) └─ AST
출처: 공식 문서
스코프 트리를 다이어그램으로 표시하면 아래와 같다
[Global Scope] ├─ bindings: { foo: var, A: function, x: let } └─ children: └─ [Function Scope A] ├─ bindings: { y: var, z: let } └─ children: └─ [Block Scope {...}]
이후 엔진에서 컴파일된 코드를 실행하는 과정에서
인터프리터(Ignition) 모드인지, JIT(Just-In-Time)모드인지에 따라
하게 된다.
함수 객체가 만들어질 때, 엔진은 "그 함수가 정의된 스코프(정의 시점의 렉시컬 환경)"에 대한 참조를 함수에 붙인다.
[[Environment]]
구조체라는 곳에 자신이 정의된 환경(상위 스코프)를 저장한다.
이것 덕분에 함수는 "자신이 정의된 위치의 스코프로 거슬러 올라가" 식별자를 찾을 수 있음.
const x = 1;
function foo() {
const x = 10;
// 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
// 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
bar();
}
// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 [[Environment]]에 저장하여 기억한다.
function bar() {
console.log(x);
}
foo(); // 1
bar(); // 1
해당 코드는 아래와 같은 그림으로 표현할 수 있다.
foo
와 bar
함수에 존재하는 [[Environment]]
구조체에는 함수 정의가 평가된 시점, 즉 전역 코드 평가 시점에 실행 컨텍스트의 렉시컬 환경인 전역 렉시컬 환경의 참조가 저장된다.
이 상황에서 함수가 호출되면 함수 내부로 코드의 제어권이 이동한다.(실행 단계)
그리고 아래 순서와 같이 함수 코드를 평가하기 시작한다.
이 때 2.3. 외부 렉시컬 환경에 대한 참조에는 아까 저장했던 [[Environment]]
구조체의 렉시컬 환경의 참조가 할당된다.(그림에서 ②와 ③에 해당됨)
함수가 호출되면 실행 컨텍스트를 만들고 새 렉시컬 환경을 설정한다.
bar
함수를 호출하면 렉시컬 환경이 포함하는 Environment Record
에 x
라는 식별자가 존재하는지 파악한 후 존재하지 않을 경우 상위(Outer
) 스코프로 이동하여 x
를 찾는다.
이 참조가 나중에 클로저가 되는 토대다. (실행 시점이 아니라 정의 시점이 핵심!)
앞서 1-3에서 설명했던 내용들 중
이 상황에서 함수가 호출되면 함수 내부로 코드의 제어권이 이동한다.(실행 단계)
에 대한 내용을 좀 더 자세히 살펴보자
Environment Record
: 실제 바인딩 테이블(예: {a: 10, b: function...}
)Outer Lexical Environment Reference
: 바깥 스코프를 가리키는 포인터(체인)undefined
)TDZ(Temporal Dead Zone)
에 있다가 실제 선언문 평가(실행) 지점에서 초기화됨.Outer Lexical Environment Reference
를 따라 부모 렉시컬 환경으로 올라간다.ReferenceError
.예시 코드
let a = 1;
function outer() {
let b = 2;
function inner() {
let c = 3;
console.log(a, b, c); // 1 2 3
}
inner();
}
outer();
const x = 10;
function foo() { console.log(x); }
// IIFE(Immediately Invoked Function Expression, 즉시 실행 함수)
(function () {
const x = 99;
foo(); // 10 (호출 위치가 아니라, foo "정의 위치"의 x를 본다)
})();
x=99
가 있어도 무시하며 렉시컬 스코프는 정의 시점으로 캡쳐된 체인만 본다는 사실을 알 수 있다.예시 코드
function makeCounter() {
let count = 0; // ← 외부 렉시컬 환경의 바인딩
return function () {
count += 1; // ← 외부 바인딩에 접근
return count;
};
}
const inc = makeCounter();
console.log(inc()); // 1
console.log(inc()); // 2
makeCounter
함수가 끝나도, 반환된 함수(클로저)가 외부 바인딩을 참조하고 있으므로 GC가 수거하지 않는다.inc = null
) GC 대상이 됨.[코드] ──파싱──▶ [AST]
│
▼
[스코프 트리 구성]
├─ Global Scope (ER)
└─ Function/Block Scopes (ER...)
│
(함수 정의 시) 함수객체 ←─── 연결 ─── 정의 시점의 LE
│
▼
[실행 컨텍스트 스택]
push EC ──▶ {
LexicalEnvironment → (현재 ER)
OuterLexicalEnvironmentReference → (부모 ER) ...
}
│
[식별자 해석 알고리즘]
현재 ER → Outer → Outer ... → Global ER(여기서도 못 찾는다면) → ReferenceError
함수 정의 시점: 함수객체.[[[Environment]]] = (정의 위치의 LE˜)
함수 호출 시점: 새 EC 생성
EC.LE = 새 환경(파라미터/로컬)
EC.LE.Outer = 함수객체.[[[Environment]]] ← 정의 시점 체인을 잇는다!
[[Environment]]
설정