클로저에 대하여

클로저

앞에서 살펴봤던 실행 컨텍스트에서 클로저에 대해 잠깐 언급했다. 실행 컨텍스트가 소멸하여도 그 환경은 남아 외부에서 참조가 가능하다고 했었지만, 사실 말도 너무 어렵고 이해하기가 어렵다.(저도 그랬습니다… 아직도 완벽하다고 보긴 어렵기도 하고…)

function outFun() {
  var x = 1;
  var inFun = function() {
    console.log(x);
  };
  return inFun;
};

var inner = outFun();
inner();

이 함수를 살펴보자. 함수 outFun()은 내부함수 inFun을 반환하고 소멸했다. 마지막에 함수 outFun()을 할당해준 변수를 소환해도 변수 x에는 접근할 수 없을 것 같지만 결과는 1이 나온다.
이처럼 자신을 포함하고 있는 외부함수가 내부함수보다 더 오래 유지되는 경우(그 환경이) 내부함수가 노출되더라도 외부함수의 지역 변에 접근할 수 있는 것을 클로저(Closure) 라고 한다.

다른 정의로는 내부함수가 그 렉시컬 환경(선언 됐을 때 환경)의 스코프를 기억하여 자신의 선언됐을 때 환경 밖에서 호출되어도 그 환경에 접근할 수 있는 함수를 말한다. 즉 자신의 태어났을 때의 환경을 기억하는 함수라고 할 수 있다.

실행컨텍스트

내부함수가 유효한 상태에서 외부함수가 종료되어 외부함수의 실행 컨텍스트가 반환되어도, 외부함수 실행 컨텍스트 내의 활성 객체는 내부함수가 참조하는 한 내부함수가 스코프체인을 통해 참조할 수 있다는 것을 의미한다.
내부함수가 하나 이상 존재하는 경우 계속 유지된다. 이 때 내부함수는 외부함수의 실제 변수에 접근한다.

클로저 활용하기

  1. 전역 변수 억제

단 이 코드의 문제는 함수 increase 함수가 호출되기 전 counter의 값이 반드시 0이여야 한다는 것이다. 그렇다고 변수 counter를 increase 함수 안에 넣는다면 함수가 호출될 때마다 값이 초기화가 된다.

이 코드를 클로저를 활용해 문제를 해결하면

즉시실행함수는 한 번만 실행되기 때문에 counter가 초기화 될 일이 없다. 또한 increaser에 담겨있는 함수가 외부함수 변수 counter에 접근할 수 있다는 것이다. 또한 변수 counter는 private 변수이므로 외부에서의 변경을 신경쓸 필요가 없다. 여기서의 counter가 바로 자유변수이다.

  1. 특정 함수에 사용자가 정의한 객체 메서드 연결

예제를 살펴보면

function HelloFunc(func) {
  this.greeting = 'Helle';
}

HelloFunc.prototype.call = function(func) {
  func ? func(this.greeting) : this.func(this.greeting);
}

var user = function(greeting) {
  console.log(greeting); // Hello
}

var objHello = new HelloFunc();
objHello.func = user;
objHello.call();

func 프로퍼티에 참조되는 함수를 call() 함수로 호출하여 func 프로퍼티에 자신이 정의한 함수를 참조시켜 호출할 수 있다.
다만 greeting만을 인자로 넣어 사용하기 때문에 한 개의 인자를 받는 함수를 정의할 수 밖에 없기 때문에

function saySomething(obj, methodName, name) {
  return (function(greeting) {
    return obj[methodName](greeting,name);
  });
}

function newObj(obj, name) {
  obj.func = saySomething(this, "who", name);
  return obj;
}

newObj.prototype.who = function(greeting, name) {
  console.log(greeting + " " + (name || "everyone") );
}

var obj1 = new newObj(objHello, "Jang");
obj1.call();
console.log(obj1);

첫 번째로 obj.func가 실행되어 saySomething() 함수에서 반환되는 함수를 참조한다. 결국 obj1은 objHello 객체에서 func 프로퍼티에 참조된 함수만 바뀐 객체가 되기 때문에 call 함수를 사용할 수 있다.

출력결과

Helle
Helle Jang
HelloFunc { greeting: ‘Helle’, func: [Function] }

또한 obj1.call()로 인해 실행되는 것은 실제로 newObj.prototype.who()가 된다. who를 HelloFunc에 연결할 수 있고 클로저는 saySomething()에서 반환되면 function(greeting) {}이 되어 자유변수 obj, methodName, name을 참조하게 된다.

  1. 함수 캡슐화
    다음 예제를 통해 자기소개를 출력해보자.

    var getCompleted = (function() {
    var buffAr = [

    'I am ',
    '',
    'I live in ',
    '',
    '. I\'m ',
    '',
    ' years old.',
    

    ];
    return (function(name, city, age) {

    buffAr[1] = name;
    buffAr[3] = city;
    buffAr[5] = age;
    
    return buffAr.join('');
    

    });
    })();

    var str = getCompleted(‘Jang’, ‘Incheon’, 27);
    console.log(str);

변수 getComplted에 익명 함수를 즉시 실행함수로 실행시켜서 반환되는 함수를 할당하는 것이다. 여기서 반환되는 함수가 클로저가 되서 자유변수 buffAr을 스코프체인에서 참조할 수 있다.

  1. setTimeout()

첫번째로 함수 fade가 document.body를 인자로 전달받아서 호출한다. 그 후 함수 fade의 지역변수인 level이 1로 초기화가 되어 있다. step은 내부함수, 외부 함수 fade의 지역변수 level을 사용하고 level은 자유 변수다.
setTimeout() 호출 후에는 fade 함수는 종료되지만, 100ms 이후에 함수 step이 호출된다.
step은 지역 변수 hex를 갖고 16진수 문자열이다. fade의 배경색을 변경할 수 있다.
그 후 변수 level이 15보다 작은지 확인한다. 그 후 level = 1을 증가시켜 작업을 반복한다.

  1. 실수

클로저의 프로퍼티값이 쓰기 가능하면 여러번 호출로 값이 항상 변할 수 있다.

function outerFun(argNum) {
  var num = argNum;
  return function(x) {
    num += x;
    console.log('num : ' + num);
  }
}

var exam = outerFun(40);
exam(5); // 45
exam(-5); //40

exam을 호출할 때마다 num의 값이 변한다.

하나의 클로저가 여러 함수 객체의 스코프 체인에 들어있는 경우.

function func() {
  var x = 1;
  return {
    func1 : function(){ console.log(++x); },
    func2 : function(){ console.log(-x); }
  };
};

var exam = func();
exam.func1(); // 2
exam.func2(); // -2

함수 호출 때마다 값이 변하므로 주의.

루프 안 클로저 활용.

var arr = [];

for(var i = 0; i < 5; i++) {
  arr[i] = function() {
    return i;
  };
}

for (var j=0; j < arr.length; j++) {
  console.log(arr[j]());
} // 55555

배열 arr 안에 5개의 함수가 할당되지만 변수 i가 전역변수이기 때문에 이미 길이가 5가 된 상태이기 때문이다.
이를 해결하기 위해서

var arr = [];

for (var i = 0; i < 5; i++) {
  arr[i] = (function (id) {
    return function() {
      return id;
    };
  }(i));
}

for (var j=0; j < arr.length; j++) {
  console.log(arr[j]());
} // 01234

이런 식으로 즉시 실행 함수를 사용하여 매개변수 id를 자유변수를 만들어 반환하면 id의 값이 유지된다.

물론 ES6의 let 키워드를 사용하면 말끔히 해결된다.(일시적 사각지대가 생기기 때문에.)

다음 시간에는 객체지향 프로그래밍의 개념에 대해 알아보겠습니다.