[Modern JS Deep Dive] CH24-클로저

Boo Sung Jun·2022년 7월 22일
0

JavaScript

목록 보기
16/27
post-thumbnail

Modern JavaScript Deep Dive 스터디 - CH24 클로저

참고 자료: ⟪모던 자바스크립트 Deep Dive⟫"(이웅모 지음,위키북스, 2020)


  • 클로저(closure): 함수와 그 함수가 선언된 렉시컬 환경과의 조합(MDN)

1. 렉시컬 스코프

  • 자바스크립트 엔진은 함수를 어디서 호출했는지가 아닌, 어디에 정의했는지에 따라 상위 스코프를 결정.
    • 이를 렉시컬 스코프(정적 스코프)라 함
  • 함수의 상위 스코프는 함수를 정의한 위치에 의해 정적으로 결정되고 변하지 않음
  • 스코프의 실체는 실행 컨텍스트렉시컬 환경
  • 렉시컬 환경은 자신의 외부 렉시컬 환경에 대한 참조를 통해 상위 렉시컬 환경과 연결 -> 스코프 체인
  • 함수의 상위 스코프를 결정한다 === 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조값을 결정한다

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

  • 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장
  • [[Environment]]에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프
  • 자신이 호출 되었을 때 생성될 함수 렉시컬 환경외부 렉시컬 환경에 대한 참조에 저장될 참조값
const x = 1;

function foo() {
  const x = 10;

  // 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
  // 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
  bar();
}

// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 [[Environment]]에 저장하여 기억한다.
function bar() {
  console.log(x);
}

foo(); // ?
bar(); // ?

3. 클로저와 렉시컬 환경

1) 클로저(closure)

  • 함수와 그 함수가 선언된 렉시컬 환경과의 조합

자바스크립트는 모든 함수의 상위 스코프를 기억하기 때문에 이론적으로는 모든 함수는 클로저라고 할 수 있음 BUT 일반적으로 모든 함수를 클로저라 하지 않음

  • 외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명주기가 종료한 외부 함수의 변수를 참조할 수 있음 -> 이러한 중첩 함수가 클로저
const x = 1;

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

// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer(); // ③
innerFunc(); // ④ 10
// inner 함수가 이미 종료된 외부 함수의 변수를 참조 -> 클로저

2) 중첩 함수가 클로저라 하기 어려운 경우

  • 상위 함수보다 오래 유지 되었지만, 상위 스코프의 어떤 식별자도 참조하지 않은 경우
function foo() {
  const x = 1;
  const y = 2;

  // 일반적으로 클로저라고 하지 않는다.
  function bar() {
    const z = 3;

    debugger;
    // 상위 스코프의 식별자를 참조하지 않는다.
    console.log(z);
  }

  return bar;
}

const bar = foo();
bar();
  • 외부 함수보다 중첩 함수의 생명주기가 짧은 경우
function foo() {
  const x = 1;

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

foo();

3) 자유 변수

  • 클로저에 의해 참조되는 상위 스코프의 변수
  • 클로저 === 자유 변수에 묶여있는 함수
  • 자바스크립트 엔진은 클로저가 참조하지 않은 식별자는 기억하지 않음

4. 클로저의 활용

  • 상태(state)를 안전하게 변경하고 유지하기 위해 사용
    • 클로저 활용 예시
// 카운트 상태 변경 함수
const increase = (function () {
  // 카운트 상태 변수
  let num = 0;

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

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
    • 클로저를 사용하지 않는 경우
// 카운트 상태 변경 함수
const increase = function () {
  // 카운트 상태 변수
  let num = 0;

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

// 이전 상태를 유지하지 못한다.
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
  • 특정 함수에게만 상태 변경을 허용 하기 위해 사용
    • 클로저 활용 예시
// ===== 클로저 활용 예시 =====
// 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 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
    • 클로저를 사용하지 않는 경우
const counter = (function () {
  // 카운트 상태 변수
  let num = 0;

  // 클로저인 메서드를 갖는 객체를 반환한다.
  // 객체 리터럴은 스코프를 만들지 않는다.
  // 따라서 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경이다.
  return {
    // num: 0, // 프로퍼티는 public하므로 은닉되지 않는다.
    increase() {
      return ++num;
    },
    decrease() {
      return num > 0 ? --num : 0;
    }
  };
}());

console.log(counter.increase()); // 1
console.log(counter.increase()); // 2

console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
const Counter = (function () {
  // ① 카운트 상태 변수
  let num = 0;

  function Counter() {
    // this.num = 0; // ② 프로퍼티는 public하므로 은닉되지 않는다.
  }

  Counter.prototype.increase = function () {
    return ++num;
  };

  Counter.prototype.decrease = function () {
    return num > 0 ? --num : 0;
  };

  return Counter;
}());

const counter = new Counter();

console.log(counter.increase()); // 1
console.log(counter.increase()); // 2

console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 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

5. 캡슐화와 정보 은닉

  • 캡슐화: 객체의 상태를 나타내는 프로퍼티 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶은것
  • 정보 은닉: 캡슐화를 객체의 특정 프로퍼티와 메서드를 감출 목적으로 사용하는 것
    • 외부로 부터 객체의 상태가 변경되는 것을 방지해 정보를 보호
    • 객체 간의 상호 의존성, 즉 결합도를 낮추는 효과

6. 자주 발생하는 실수

  • var 키워드로 선언한 변수 는 함수 레벨 스코프 -> 블록 레벨 스코프로 사용 시 주의
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]());
}
// 0, 1, 2 가 아닌 3, 3, 3 이 출력
// var 키워드는 전역 변수이므로 나중 값이 출력됨

0개의 댓글