Closure
- 면접 단골 토픽 클로저… 이제 좀 친해지자!
어휘적 범위지정 (lexical scoping)
변수가 어디에서 사용 가능한 지 알기 위해 그 변수가 소스 코드 내에 어디서 선언되었는지 고려한다
스코프는 함수를 호출할 때가 아니라 선언할 때 생긴다 ⇒ 정적 스코프
↔ 동적 스코프: 함수를 어디서 호출하였는지에 따라 상위 스코프가 결정되는 것
var name = 'zero';
function log() {
console.log(name);
}
function wrapper() {
name = 'nero';
log();
}
wrapper(); // nero
var name = 'zero';
function log() {
console.log(name);
}
function wrapper() {
var name = 'nero';
log();
}
wrapper(); // zero => 스코프를 선언할 때 생기기 때문에
function init() {
var name = "Mozilla"; // name is a local variable created by init
function displayName() { // displayName() is the inner function, a closure
alert (name); // displayName() uses variable declared in the parent function
}
displayName();
}
init();
부모 함수에서 정의한 변수 name 값을 displayName()에서 출력
⇒ 함수가 중첩된 상황에서 외부 범위에서 선언된 변수에도 접근 가능 (scope chain을 통해 접근)
closure?
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
//myFunc변수에 displayName을 리턴함
//유효범위의 어휘적 환경을 유지
myFunc();
//리턴된 displayName 함수를 실행(name 변수에 접근)
makeFunc에서 displayName 함수를 리턴함
리턴하는 함수는 클로저를 형성
⇒ 클로저: 함수와 함수가 선언된 어휘적 환경의 조합
⇒ 함수 밖에서 선언된 변수를 함수 내부에서 사용할 때 클로저 생겨남
displayName의 인스턴스는 변수 name이 있는 어휘적 환경에 대한 참조를 유지함
myFunc가 호출될 때 변수 name은 사용할 수 있는 상태로 남게되어 Mozilla가 alert에 전달되는 것
클로저 이용하여 private method 흉내내기
var counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
};
})();
console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1
counter.increment / counter.decrement / counter.value를 통해 공유되는 하나의 어휘적 환경이 익명함수 안에서 만들어진다
두개의 private item인 privateCounter와 changeBy function 포함
얘네는 익명 wrapper에서 반환된 counter.increment / counter.decrement / counter.value 퍼블릭 함수를 통해서만 접근 가능
각 클로저는 그들 고유의 클로저의 변수를 참조함 ⇒ 하나의 클로저에서 변수 값을 변경해도 다른 클로저의 값에서는 영향을 주지 않는다
⇒ 객체지향 프로그래밍의 encapsulation같은 이점 얻을 수 있음
-) 단점: 잘못사용 했을 때 성능/메모리 문제
비공개 변수는 자바스크립트에서 언제 메모리 관리를 해야할 지 모르기 때문에 메모리 낭비로 이루어질 수 있다
loop와 클로저
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();
showHelp()는 세번 다 age관련 필드만 보여줌
⇒ onFocus이벤트에 연결된 함수가 클로저이기 때문
이 코드를 사용하면 제대로 동작하지 않는 것을 알게 된다. 어떤 필드에 포커스를 주더라도 나이에 관한 도움말이 표시된다. onfocus 이벤트에 연결된 함수가 클로저이기 때문이다. 이 클로저는 함수 정의와 setupHelp 함수 범위에서 캡처된 환경으로 구성된다. 루프에서 세 개의 클로저가 만들어졌지만 각 클로저는 값이 변하는 변수가 (item.help) 있는 같은 단일 환경을 공유한다. onfocus 콜백이 실행될 때 콜백의 환경에서 item 변수는 (세개의 클로저가 공유한다) helpText 리스트의 마지막 요소를 가리키고 있을 것이다.
lexical scoping에 따라 함수는 선언할 때 스코프가 생성됨 ⇒ 이벤트리스너 안의 item은 외부의 item을 계속 참조하고 있는 것
해결책 1 : 추가적 콜백 function 사용
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function makeHelpCallback(help) {
return function() {
showHelp(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 = makeHelpCallback(item.help);
}
}
setupHelp();
해결책 2: 익명 클로저 사용
⇒ for 문 내 코드 전체를 함수로 감싸서 각각의 콜백에 새로운 어휘적 환경을 생성
IIFE ⇒ 고정된 item에 대한 클로저인 function을 만드는 셈
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++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})(); // Immediate event listener attachment with the current value of item (preserved until iteration).
}
}
setupHelp();
해결책 3: var 대신 let 사용