본문 바로가기
JS

모던 자바스크립트 Deep Dive - 클로저

by 학식러 2024. 1. 19.

 

24장 클로저

 

렉시컬 스코프

  • 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다.

 

함수 객체의 내부 슬롯 [[Environment]]

  • 함수는 자신이 정의된 환경, 즉 상위 스코프를 기억해야함.
  • 이를 위해 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장함.

 

클로저와 렉시컬 환경

24-05

const x = 1;

// ①
function outer() {
  const x = 10;
  const inner = function () { console.log(x); }; // ②
  return inner;
}

// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer(); // ③
innerFunc(); // ④ 10
  • 이미 종료되어 실행 컨텍스트 스택에서 제거된 outer함수의 x가 동작함.
  • 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 외부함수의 변수를 참조할 수 있음. 이러한 중첩함수 : 클로저
  • outer 함수가 종료되면 inner 함수 반환하면서 종료된다. 즉 outer 함수의 실행 컨텍스트가 스택에서 제거됨.
  • 이 때 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다.
  • 클로저 : 중첩함수가 상위 스코프의 식별자를 참조하고 있고 중첩함수가 외부 함수보다 더 오래 유지하는 경우에 한정함.
  • 자유변수 : 클로저에 의해 참조되는 상위 스코프의 변수(예제의 x)
  • 클로저 : 자유변수에 묶여있는 함수.

24-08

<!DOCTYPE html>
<html>
<body>
  <script>
    function foo() {
      const x = 1;
      const y = 2;

      // 클로저
      // 중첩 함수 bar는 외부 함수보다 더 오래 유지되며 상위 스코프의 식별자를 참조한다.
      function bar() {
        debugger;
        console.log(x);
      }
      return bar;
    }

    const bar = foo();
    bar();
  </script>
</body>
</html>

 

클로저x

24-07

<!DOCTYPE html>
<html>
<body>
  <script>
    function foo() {
      const x = 1;

      // 일반적으로 클로저라고 하지 않는다.
      // bar 함수는 클로저였지만 곧바로 소멸한다.
      function bar() {
        debugger;
        // 상위 스코프의 식별자를 참조한다.
        console.log(x);
      }
      bar();
    }

    foo();
  </script>
</body>
</html>

 

클로저의 활용

  • 사용목적 : 상태를 안전하게 변경하고 유지하기 위해= 특정 함수에게만 상태 변경을 허용

24-09

// 카운트 상태 변수
let num = 0;

// 카운트 상태 변경 함수
const increase = function () {
  // 카운트 상태를 1만큼 증가 시킨다.
  return ++num;
};

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
  • num은 increase()만 변경시킬 수 있어야한다.(의미적으로) 의도치 않게 num을 변경시키면 오류로 이어짐.

 

24-10

// 카운트 상태 변경 함수
const increase = function () {
  // 카운트 상태 변수
  let num = 0;

  // 카운트 상태를 1만큼 증가 시킨다.
  return ++num;
};

// 이전 상태를 유지하지 못한다.
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
  • 지역변수로 바꾸는 방법은 이전 상태를 유지하지 못함.

 

24-11

// 카운트 상태 변경 함수
const increase = (function () {
  // 카운트 상태 변수
  let num = 0;

  // 클로저
  return function () {
    // 카운트 상태를 1만큼 증가 시킨다.
    return ++num;
  };
}());

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
  • 즉시 실행 함수가 반환한 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하고 있음.

 

클로저 함수 예제

24-14

// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
function makeCounter(aux) {
  // 카운트 상태를 유지하기 위한 자유 변수
  let counter = 0;

  // 클로저를 반환
  return function () {
    // 인수로 전달 받은 보조 함수에 상태 변경을 위임한다.
    counter = aux(counter);
    return counter;
  };
}

// 보조 함수
function increase(n) {
  return ++n;
}

// 보조 함수
function decrease(n) {
  return --n;
}

// 함수로 함수를 생성한다.
// makeCounter 함수는 보조 함수를 인수로 전달받아 함수를 반환한다
const increaser = makeCounter(increase); // ①
console.log(increaser()); // 1
console.log(increaser()); // 2

// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동하지 않는다.
const decreaser = makeCounter(decrease); // ②
console.log(decreaser()); // -1
console.log(decreaser()); // -2
  • makeCounter함수를 호출해 함수를 반환할 때 반환된 함수는 자신만의 독립된 렉시컬 환경을 갖는다.= increaser, decreaser에 할당된 함수는 독립된 렉시컬 환경을 가짐.⇒ makeCounter 함수를 두 번 호출하면 안됨.
    // 함수를 반환하는 고차 함수
    // 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
    const counter = (function () {
      // 카운트 상태를 유지하기 위한 자유 변수
      let counter = 0;
    
      // 함수를 인수로 전달받는 클로저를 반환
      return function (aux) {
        // 인수로 전달 받은 보조 함수에 상태 변경을 위임한다.
        counter = aux(counter);
        return counter;
      };
    }());
    
    // 보조 함수
    function increase(n) {
      return ++n;
    }
    
    // 보조 함수
    function decrease(n) {
      return --n;
    }
    
    // 보조 함수를 전달하여 호출
    console.log(counter(increase)); // 1
    console.log(counter(increase)); // 2
    
    // 자유 변수를 공유한다.
    console.log(counter(decrease)); // 1
    console.log(counter(decrease)); // 0
  • 24-15
  • 자유변수 counter를 공유하지 않아서 카운터 증감이 연동되지 않는 문제.

 

캡슐화와 정보 은닉

  • 캡슐화 : 객체의 상태를 나타내는 프로퍼티, 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드하나로 묶는 것
  • 정보 은닉 : 캡슐화를 특정 프로퍼티나 메서드를 감출 목적으로 사용결합도(객체간의 상호 의존성)을 낮추는 효과.

 

자주 발생하는 실수

24-20

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = function () { return i; }; // ①
}

for (var j = 0; j < funcs.length; j++) {
  console.log(funcs[j]()); // ②
}
  • i는 함수스코프이기 때문에 전역변수.

 

클로저로 해결.

24-21

var funcs = [];

for (var i = 0; i < 3; i++){
  funcs[i] = (function (id) { // ①
    return function () {
      return id;
    };
  }(i));
}

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

 

let으로 해결.

24-22

const funcs = [];

for (let i = 0; i < 3; i++) {
  funcs[i] = function () { return i; };
}

for (let i = 0; i < funcs.length; i++) {
  console.log(funcs[i]()); // 0 1 2
}

댓글