한줄 요약
함수가 실행 될 때, 어휘적 범위를 기억하고 실행되는 어휘적 범위 묶음과 그 함수
어휘적 범위 지정(lexical scoping)
js 파서가 변수를 처리할 때, 변수가 어디에서 사용 가능한지 알기 위해 그 변수가 소스코드 내 어디에서 선언되었는지 고려한다는 것
function init() { var name = "Mozilla"; // name은 init에 의해 생성된 지역 변수이다. function displayName() { // displayName() 은 내부 함수이며, 클로저다. alert(name); // 부모 함수에서 선언된 변수를 사용한다. } displayName(); } init();
내부 함수
displayName()
에는 name
이라는 지역 변수가 없지만, js 에서는 Mozilla 로 alert 을 띄워줍니다. 어휘적 범위 지정은 전역 범위, 외부 함수 범위, 지역 범위 3가지로 구분하여 지정합니다.렉시컬 환경(lexical environment)
자바스크립트에서는 어휘적 범위 지정을 위한 렉시컬 환경이라는 내부 숨김 연관 객체를 사용합니다. 이는 스크립트 전체, 함수, 코드 블록이 실행 될 때 생기며, 이를 통해 변수를 관리하게 됩니다.
렉시컬 환경 객체의 구성
- 환경 레코드(Environment Record): 모든 지역 변수를 프로퍼티로 저장하고 있는 객체, this 값과 같은 기타 정보도 여기에 저장됩니다.
- 외부 렉시컬 환경에 대한 참조: 상위 스코프에 대한 환경 레코드 참조를 가지고있습니다.
변수란 사실 특수 내부 객체인 렉시컬 환경의 환경 레코드 프로퍼티일 뿐입니다. 변수를 가져오거나 변경 하는 것은 환경 레코드의 프로퍼티를 가져오거나 변경하는 것입니다.
- 변수에 대한 렉시컬 환경
자바스크립트의 스크립트 코드가 실행 시작되면 스크립트 내에 선언한 변수 전체가 렉시컬 환경에 올라갑니다. (pre-populated) 이를 호이스팅이라고도 합니다. 이때 변수의 상태는 특수 내부 상태인
uninitialized
가 됩니다. 자바스크립트 엔진은 uninitialized
상태의 변수를 인지하긴 하지만, let foo
와 같은 선언문을 만나기 전까진 이 변수를 참조할 수 없습니다.- 함수에 대한 렉시컬 환경
함수 선언문으로 선언한 함수는 일반 변수와 달리 바로 초기화가 됩니다. 그래서 호이스팅 된 것 처럼 코드 아래에 선언 되어도 호출할 수 있습니다.
foo(); function foo() { console.log('foo'); }
하지만, 변수에 할당한 함수 표현식은 바로 초기화 되지 않습니다.
bar(); const bar = () => { console.log('bar'); }
- 내부와 외부의 렉시컬 환경
코드에서 변수에 접근할 때는, 자신의 렉시컬 환경을 먼저 보고, 이후 외부 렉시컬 환경을 타고타고 올라가 검색합니다. 이는 전역 렉시컬 환경까지 다 검색 할 때까지 반복됩니다.
- 함수를 반환하는 함수의 렉시컬 환경
함수를 반환하는 함수에선 어떻게 렉시컬 환경이 구성될까?
function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter();
해당 코드에서
let counter = makeCounter()
문에서 makeCounter()
가 실행되면 makeCounter
함수에 대한 let count
변수를 담은 렉시컬 환경 객체가 생긴다. 이후, return 문에서 함수가 실행 될 때, makeCounter
의 렉시컬 환경을 외부 렉시컬 환경 객체 참조로 가지는 렉시컬 환경이 구성된다.함수는
[[Environment]]
라는 숨김 프로퍼티를 갖는데, 이곳에 함수가 만들어진 당시의 렉시컬 환경에 대한 참조가 저장된다. return 문의 내부 함수에는 아무 지역 변수가 없으므로 렉시컬 환경은 비어있지만, 외부 렉시컬 환경들은 아래 그림과 같은 형태가 될 것이다.그렇다면 해당 코드는 어떻게 실행 될까?
"use strict"; let x = 1; function func() { console.log(x); // ? let x = 2; } func();
정답
재선언 에러가 아닌 ReferenceError 가 나는 이유는 렉시컬 환경에서 이미 let x = 2; 구문에 대한 정보가 환경 레코드로 들어가 있어서 x 를 외부 렉시컬 환경에서 끌어오지 않기 때문이다.
클로저(closure)
function makeFunc() { var name = "Mozilla"; function displayName() { alert(name); } return displayName; } var myFunc = makeFunc(); //myFunc변수에 displayName을 리턴함 //유효범위의 어휘적 환경을 유지 myFunc(); //리턴된 displayName 함수를 실행(name 변수에 접근)
다른 언어에서는 함수가 실행 종료되면 지역 변수가 메모리에서 내려가기 때문에, 해당 코드가 동작 하는 것이 어색할 수 있다. 하지만, 자바스크립트에서는 클로저를 형성하기 때문에, 해당 코드가 제대로 동작 할 수 있다. 클로저는 어떤 함수의 내부에 들어있는 함수와 그 함수가 영향을 미치는 변수 환경 조합을 합쳐 이르는 말이다. 내부함수
displayName()
은 클로저를 형성하여 name
을 기억하고 접근할 수 있으므로 alert(name)
을 출력 할 수 있게 된다.function makeAdder(x) { var y = 1; return function(z) { y = 100; return x + y + z; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); //클로저에 x와 y의 환경이 저장됨 console.log(add5(2)); // 107 (x:5 + y:100 + z:2) console.log(add10(2)); // 112 (x:10 + y:100 + z:2) //함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산
add5
와 add10
은 클로저이다. (사실 자바스크립트의 모든 함수들은 클로저이다.) 그래서 makeAdder
의 지역변수 y 를 기억해 해당 변수를 클로저가 변경 한다.function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16); document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16;
클로저의 활용 예시
클로저를 이용해서 프라이빗 메소드 흉내내기
var makeCounter = function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } }; var counter1 = makeCounter(); var counter2 = makeCounter(); alert(counter1.value()); /* 0 */ counter1.increment(); counter1.increment(); alert(counter1.value()); /* 2 */ counter1.decrement(); alert(counter1.value()); /* 1 */ alert(counter2.value()); /* 0 */
두 개의 카운터가 서로 다른 카운터와 독립성을 유지하는 방법은, 클로저들은 생성 될 때 환경만을 기억해 오고, 그 이후에는 자신의 클로저 환경을 변경하는 방식으로 사용 하기 때문이다. 이런 디자인 패턴을 모듈 패턴이라고 한다.
루프에서 클로저 생성하기: 일반적인 실수
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
어떤 요소에 focus 되면 help 메시지가 보여지도록 하는 코드인데, 이는 제대로 동작하지 않는다!!
어느 요소를 focus 해도 age 부분만 help 메시지로 뜨게 된다. 왜냐하면 for문 안에 있는 클로저들이 생성이 될 때, item 변수가 포함된 단일 환경을 공유하기 때문이다. 이는 for loop block 안에 있는 var item 을 let 으로 선언하게 되면 해결된다.
출처