inner는 자기가 호출된 위치가 아니라 정의된 위치의 스코프를 봐요. 정의된 위치에선 outer의 EnvRec이 상위 스코프고, 거기엔 x: 2가 있어요. 전역 x: 1은 더 위에 있지만, inner는 outer의 EnvRec에서 먼저 x를 찾았기 때문에 거기서 검색을 멈춰요.
이게 렉시컬 스코프의 정의예요. 동적 스코프 언어였다면 호출 시점의 환경을 봐서 1이 나왔을 거예요.
var는 함수 스코프예요. 그래서 i는 makeCounters 전체에서 하나뿐인 변수고, 세 화살표 함수가 모두 같은 EnvRec의 같은 binding을 캡처해요. 루프가 끝나면 i는 3이 돼있고, 나중에 함수를 호출하면 셋 다 그 3을 봐요.
var를 let으로 바꾸면 답이 0 1 2로 바뀌어요. let은 블록 스코프라서 매 반복마다 새로운 EnvRec이 생기고, 각 화살표 함수가 서로 다른 binding을 캡처하거든요. 14장 var 얘기랑 직결돼요.
huge 배열은 GC될까요?명세대로라면 fn이 outer의 EnvRec 전체를 잡고 있어서 huge도 살아남아야 해요. 하지만 V8 같은 현대 엔진은 Escape Analysis로 "이 클로저가 실제로 참조하는 변수는 small뿐"임을 알아채고, huge는 GC 대상으로 처리해요.
다만 eval이 클로저 안에 있으면 V8도 EnvRec 전체를 보존해야 해서 최적화가 깨져요. eval이 위험한 이유 중 하나가 이거예요 — 정적 분석을 망가뜨려요.
var로 선언한 전역은 Object Environment Record(globalThis를 감싸는 환경)에 들어가요. 그래서 globalThis의 프로퍼티가 돼요. let/const로 선언한 전역은 별도의 Declarative Environment Record(Script Record)에 들어가요. 같은 "전역"이지만 들어가는 EnvRec이 달라요.
14장에서 var가 위험한 이유로 자주 나오는 얘기예요 — 의도치 않게 전역 객체를 오염시키니까. 13장 시스템 관점으로 보면, 이건 단순히 "오염"이 아니라 EnvRec이 두 종류라는 명세 차원의 구분이에요.