All Articles

[Javascript] Closure

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 사용