DEVELOP
article thumbnail

모던 자바스크립트 Deep Dive 를 읽고 작성한 학습용 게시글입니다.

 

모던 자바스크립트 Deep Dive: 자바스크립트의 기본 개념과 동작 원리

269개의 그림과 원리를 파헤치는 설명으로 ‘자바스크립트의 기본 개념과 동작 원리’를 이해하자! 웹페이지의 단순한 보조 기능을 처리하기 위한 제한적인 용도로 태어난 자바스크립트는 과도

wikibook.co.kr


클로저

  • 클로저는 자바스크립트 고유의 개념이 아니다.
  • 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어(하스켈, 리스프, 얼랭, 스칼라 등)에서 사용되는 중요한 특성이다.
  • MDN에서는 클로저에 대해 다음과 같이 정의하고 있다.

A closure is the combination of a function and the lexical enviornment within that funtion was declared.

클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.

  • 핵심 키워드는 함수가 선언된 렉시컬 환경 이다.
// 예제 24-01

const x = 1;
function outerFunc() {
  const x = 10;
  function innerFunc() {
    console.log(x); // 10
  }

  innerFunc();
}

outerFunc();
  • 이 같은 현상이 발생하는 이유는 자바스크립트가 렉시컬 스코프를 따르는 프로그래밍 언어이기 때문이다.

렉시컬 스코프

자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프(정적 스코프)라 한다.

// 예제 24-03

const x = 1;

function foo() {
  const x = 10;
  bar();
}

function bar() {
  console.log(x);
}

foo(); // 1
bar(); // 1
  • 위 예제에서 foo 함수와 bar 함수는 모두 전역에서 정의된 전역 함수이며, 상위 스코프는 전역이다.
  • 함수를 어디서 호출하는지는 함수의 상위 스코프 결정에 어떠한 영향도 주지 못한다.
  • 즉, 함수의 상위 스코프는 함수를 정의한 위치에 의해 정적으로 결정되고 변하지 않는다.
  • 스코프의 실체는 실행 컨텍스트의 렉시컬 환경이다.
  • 이 렉시컬 환경은 자신의 외부 렉시컬 환경에 대한 참조 를 통해 상위 렉시컬 환경과 연결된다. ⇒ 스코프 체인
  • 따라서 함수의 상위 스코프를 결정한다 는 것은 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조값을 결정한다 는 것과 같다.
  • 렉시컬 환경의 외부 렉시컬 환경에 대한 참조 에 저장할 참조값이 바로 상위 렉시컬 환경에 대한 참조이며, 이것이 상위 스코프이다.

렉시컬 환경의 외부 렉시컬 환경에 대한 참조 에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다. 이것이 바로 렉시컬 스코프이다.

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

  • 함수가 정의된 환경(위치)과 호출되는 환경(위치)은 다를 수 있다.
  • 따라서 렉시컬 스코프가 가능하려면 함수는 자신이 호출되는 환경과는 상관없이 자신이 정의된 환경, 즉 상위 스코프(함수 정의가 위치하는 스코프가 바로 상위 스코프)를 기억해야 한다.
  • 이를 위해 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경(= 상위 스코프의 참조)를 저장한다.
  • 함수 정의가 평가되어 평가되어 함수 객체를 생성할 때 자신이 정의된 환경(위치)에 의해 결정된 상위 스코프의 참조를 함수 객체 자신의 내부 슬롯[[Environment]]에 저장한다.
  • 이때 자신의 내부 슬롯 [[Environment]]에 저장된 상위 스코프의 참조는 현재 실행 중인 실행 컨텍스트의 렉시컬 환경을 가리킨다.
  • 왜냐하면, 함수 정의가 평가되어 함수 객체를 생성하는 시점은 함수가 정의된 환경, 즉 상위 함수(또는 전역 코드)가 평가 또는 실행되고 있는 시점이며, 이때 현재 실행 중인 실행 컨텍스트는 상위 함수(또는 전역 코드)의 실행 컨텍스트이기 때문이다.
  • 예를 들어, 전역에서 정의된 함수 선언문은 전역 코드가 평가되는 시점에 평가되어 함수 객체를 생성한다.
  • 이때 생성된 함수 객체의 내부 슬록 [[Environment]]에는 함수 정의가 평가되는 시점, 즉 전역 코드 평가 시점에 실행 중인 실행 컨텍스트의 렉시컬 환경인 전역 렉시컬 환경의 참조가 저장된다.
  • 함수 내부에서 정의된 함수 표현식은 외부 함수 코드가 실행되는 시점에 평가되어 함수 객체를 생성한다.
  • 이때 생성된 함수 객체의 내부 슬롯 [[Environment]]에는 함수 정의가 평가되는 시점, 즉 외부 함수 코드 실행 시점에 실행 중인 실행 컨텍스트의 렉시컬 환경인 외부 함수 렉시컬 환경의 참조가 저장된다.
  • 따라서 함수 객체의 내부 슬롯 [[Environment]]에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프다.
  • 또한 자신이 호출되었을 때 생성될 함수 렉시컬 환경의 외부 렉시컬 환경에 대한 참조 에 저장될 참조값이다.
  • 함수 객체는 내부 슬롯 [[Environment]]에 저장한 렉시컬 환경의 ㅣ참조, 즉 상위 스코프를 자신이 존재하는 한 기억한다.
// 예제 24-04

const x = 1;

function foo() {
  const x = 10;

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

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

foo(); // 1
bar(); // 1

위 예제의 foo 함수 내부에서 bar 함수가 호출되어 실행 중인 시점의 실행 컨텍스트는 다음과 같다.

그림 24-1. 함수 객체의 내부 슬롯 [[Environment]]에는 상위 스코프가 저장된다.

  • foo함수와 bar 함수 모두 전역에서 함수 선언문으로 정의되었다.
  • 따라서 foo 함수와 bar 함수는 모두 전역 코드가 평가되는 시점에 평가되어 함수 객체를 생성하고 전역 객체 window의 메서드가 된다.
  • 이때 생성된 함수 객체의 내부 슬롯 [[Environment]]에는 함수 정의가 평가된 시점, 즉 전역 코드 평가 시점에 실행 중인 실행 컨텍스트의 렉시컬 환경인 전역 렉시컬 환경의 참조가 저장된다. (위 그림에서 ①)
  • 함수가 호출되면 함수 내부로 코드의 제어권이 이동한다.
  • 그리고 함수 코드를 평가하기 시작한다.
  • 함수 코드 평가는 아래 순서로 진행된다.
    1. 함수 실행 컨텍스트 생성
    2. 함수 렉시컬 환경 생성
      1. 함수 환경 레코드 생성
      2. this 바인딩
      3. 외부 렉시컬 환경에 대한 참조 설정
  • 이때 함수 렉시컬 환경의 구성 요소인 외부 렉시컬 환경에 대한 참조에는 함수 객체의 내부 슬롯 [[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 함수를 호출(⓷)하면 outer 함수는 중첩 함수 inner를 반환하고 생명 주기를 마감한다.
  • 즉, outer 함수의 실행이 종료되면 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거(pop)된다.
  • 이때, outer 함수의 지역 변수 x와 변수 값 10을 저장하고 있던 outer 함수의 실행 컨텍스트가 제거되었으므로 outer 함수의 지역 변수 x 또한 생명 주기를 마감한다.
  • 따라서 outer 함수의 지역 변수 x는 더는 유효하지 않게 되어 x 변수에 접근할 수 있는 방법은 달리 없어 보인다.
  • 그러나 위 코드의 실행 결과(④)는 outer 함수의 지역 변수 x의 값인 10이다.
  • 이미 생명 주기가 종료되어 실행 컨텍스트 스택에서 제거된 outer함수의 지역 변수 x가 다시 부활이라도 한듯이 동작하고 있다.
  • 이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저(closure)라고 부른다.
  • 모든 함수가 기억하는 상위 스코프는 함수를 어디서 호출하든 상관없이 유지된다.
  • 따라서 함수를 어디서 호출하든 상관없이 함수는 언제나 자신이 기억하는 상위 스코프의 식별자를 참조할 수 있으며 식별자에 바인딩된 값을 변경할 수도 있다.
  • 위 예제에서 inner 함수는 자신이 평가될 때 자신이 정의된 위치에 의해 결정된 상위 스코프를 [[Environment]] 내부 슬롯에 저장한다.
  • 이때 저장된 상위 스코프는 함수가 존재하는 한 유지된다.
  • 위 예제에서 outer 함수가 평가되어 함수 객체를 생성할 때(①) 현재 실행중인 실행 컨텍스트의 렉시컬 환경, 즉, 전역 렉시컬 환경을 outer 함수 객체의 [[Environment]] 내부 슬롯에 상위 스코프로서 저장한다.

  • outer 함수를 호출하면 outer 함수의 렉시컬 환경이 생성되고 앞서 outer 함수 객체의 [[Environment]] 내부 슬롯에 저장된 전역 렉시컬 환경을 outer 함수 렉시컬 환경의 “외부 렉시컬 환경에 대한 참조”에 할당한다.
  • 그리고 중첩 함수 inner가 평가된다. (② inner 함수는 함수 표현식으로 정의했기 때문에 런타임에 평가된다.)
  • 이때 중첩 함수 inner는 자신의 [[Environment]] 내부 슬롯에 현재 실행 중인 실행 컨텍스트의 렉시컬 환경 (= outer 함수의 렉시컬 환경)을 상위 스코프로서 저장한다.

  • outer 함수의 실행이 종료하면 inner 함수를 반환하면서 outer 함수의 생명 주기가 종료된다.(⓷)
  • 즉, outer 함수의 실행 컨텍스트가 실행 컨텍스트 스택에서 제거된다.
  • 이때 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만, outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다.
  • outer 함수의 렉시컬 환경은 inner함수의 [[Environment]] 내부 슬롯에 의해 참조되고 있고 inner 함수는 전역 변수 innerFunc에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 되지 않기 때문이다.
  • 가비지 컬렉터는 누군가 참조하고 있는 메모리 공간을 함부로 해제하지 않는다.

  • outer 함수가 반환한 inner 함수를 호출하면(④) inner 함수의 실행 컨텍스트가 생성되고 실행 컨텍스트 스택에 푸시된다.
  • 그리고 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에는 inner 함수 객체의 [[Environment]] 내부 슬롯에 저장되어 있는 참조값이 할당된다.

  • 중첩 함수 inner는 외부 함수 outer보다 더 오래 생존했다.
  • 이때 외부 함수보다 더 오래 생존한 중첩 함수는 외부 함수의 생존 여부(실행 컨텍스트의 생존 여부)와 상관없이 자신이 정의된 위치에 의해 결정된 상위 스코프를 기억한다.
  • 이처럼 중첩 함수 inner의 내부에서는 상위 스코프를 참조할 수 있으므로 상위 스코프의 식별자를 참조할 수 있고, 식별자의 값을 변경할 수도 있다.
  • 자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로 모든 함수는 클로저이다.
  • 하지만 일반적으로 모든 함수를 클로저라고 하지는 않는다.
// 예제 24-06

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

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

const bar = foo();
bar();
  • 위 예제 24-6의 중첩 함수 bar는 외부 함수 foo보다 더 오래 유지되지만 상위 스코프의 어떤 식별자도 참조하지 않는다.
  • 이처럼 상위 스코프의 어떤 식별자도 참조하지 않는 경우 대부분의 모던 브라우저는 최적화를 통해 상위 스코프를 기억하지 않는다.
  • 참조하지도 않는 식별자를 기억하는 것은 메모리 낭비이기 때문이다.
  • 따라서 위 예제에서의 bar 함수는 클로저라고 할 수 없다.
// 예제 24-07

function foo() {
  const x = 1;

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

foo();
  • 위 예제의 중첩 함수 bar는 상위 스코프의 식별자를 참조하고 있으므로 클로저이다.
  • 하지만 외부 함수 foo의 외부로 중첩 함수 bar가 반환되지 않는다.
  • 즉, 외부 함수 foo보다 중첩 함수 bar의 생명 주기가 짧다.
  • 이런 경우 중첩 함수 bar는 클로저였지만 외부 함수보다 일찍 소멸되기 때문에 생명 주기가 종료된 외부 함수의 식별자를 참조할 수 있다는 클로저의 본질에 부합하지 않는다.
  • 따라서 위 예제에서의 중첩 함수 bar는 일반적으로 클로저라고 하지 않는다.
// 예제 24-08

function foo() {
  const x = 1;
  const y = 2;

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

const bar = foo();
bar();
  • 위 예제에서의 중첩 함수 bar는 상위 스코프의 식별자를 참조하고 있으므로 클로저다.
  • 그리고 외부 함수의 외부로 반환되어 외부 함수보다 더 오래 살아 남는다.
  • 이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다.
  • 이러한 중첩함수를 클로저 라고 한다.
  • 클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다.
  • 다만, 위와 같은 경우 상위 스코프의 식별자 x,y 중에서 bar는 x만을 참조하고 있는데, 이런 경우 대부분의 모던 브라우저는 최적화를 통해 참조하고 있는 식별자인 x만을 기억한다.
  • 클로저에 의해 참조되는 상위 스코프의 변수 (위 예제에서 x)를 자유 변수(free variable)이라고 부른다.
  • 클로저란 “함수가 자유 변수에 대해 닫혀있다.”는 의미이다. = “자유 변숭에 묶여있는 함수”
  • 이론적으로 클로저는 상위 스코프를 기억해야 하므로 불필요한 메모리의 점유를 걱정할 수도 있지만, 모던 자바스크립트 엔진은 최적화가 잘 되어 있어서 클로저가 참조하고 있지 않는 식별자는 기억하지 않는다.
  • 즉, 상위 스코프의 식별자 중에서 기억해야 할 식별자만 기억한다.
  • 기억해야 할 식별자를 기억하는 것은 메모리의 낭비라고 볼 수 없으며, 클로저의 메모리 점유는 필요한 것을 기억하기 위한 것이므로 이는 걱정할 대상이 아니다.
  • 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.
  • 즉, 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.
// 예제 24-09

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

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

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
  • 위 예제 24-09는 오류를 발생시킬 가능성을 내포하고 있다.
  • 그 이유는
    • 카운트 상태는 increase 함수가 호출되기 전까지 변경되지 않고 유지되어야 한다.
    • 이를 위해 카운트 상태는 increase 함수만이 변경할 수 있어야 한다.
  • 하지만 카운트 상태는 전역 변수를 통해 관리되고 있어 언제든지 누구나 접근할 수 있고 변경할 수 있다.(암묵적 결합)
  • 만약 누군가에 의해 의도치 않게 카운트 상태, 즉 전역 변수 num의 값이 변경되면 이는 오류로 이어진다.
  • 따라서 increase 함수만이 num 변수를 참조하고 변경할 수 있게 하는 것이 바람직하다.
  • 이를 위해 전역 변수 num을 increase 함수의 지역 변수로 바꾸어 의도치 않은 상태 변경을 방지해보자
// 예제 24-10

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

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

console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
  • 이제 increase 함수만이 num 변수의 상태를 변경할 수 있지만, increase 함수가 호출될 때마다 지역 변수 num은 다시 선언되고 0으로 초기화되기 때문에 출력 결과는 언제나 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-11의 코드가 실행되면 즉시 실행 함수가 호출되고 즉시 실행 함수가 반환한 함수가 increase 변수에 할당된다.
  • increase 변수에 할당된 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저이다.
  • 즉시 실행 함수는 호출된 이후 소멸되지만 즉시 실행 함수가 반환한 클로저는 increase 변수에 할당되어 호출된다.
  • 이때 즉시 실행 함수가 반환한 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하고 있다.
  • 따라서 즉시 실행 함수가 반환한 클로저는 카운트 상태를 유지하기 위한 자유 변수 num을 언제 어디서 호출하든지 참조하고 변경할 수 있다.
  • 즉시 실행 함수는 한 번만 실행되므로 increase가 호출될 때마다 num 변수가 재차 초기화될 일은 없다.
  • 또한 num 변수는 외부에서 직접 접근할 수 없는 은닉된 private 변수이므로 전역 변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요도 없기 때문에 더 안정적인 프로그래밍이 가능하다.
  • 이처럼 클로저는 상태가 의도치 않게 변경되지 않도록 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용한다.
// 예제 24-12

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

  // 클로저인 메서드를 갖는 객체를 반환한다.
  // 객체 리터럴은 스코프를 만들지 않는다.
  // 따라서 아래 메서드들의  상위 스코프는 즉시 실행 함수의 렉시컬 환경이다.
  return {
    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
  • 위 예제 24-12에서 즉시 실행 함수가 반환하는 객체 리터럴은 즉시 실행 함수의 실행 단계에서 평가되어 객체가 된다.
  • 이때 객체의 메서드도 함수 객체로 생성된다.
  • 객체 리터럴의 중괄호는 코드 블록이 아니므로 별도의 스코프를 생성하지 않는다.
  • 위 예제의 increase, decrease 메서드의 상위 스코프는 increase, decrease가 평가되는 시점에 실행중인 실행 컨텍스트인 즉시 실행 함수 실행 컨텍스트의 렉시컬 환경이다.
  • 따라서 increase, decrease 메서드가 언제 어디서 호출되든 상관없이 increase, decrease 함수는 즉시 실행 함수의 스코프의 식별자를 참조할 수 있다.
  • 위 예제를 생성자 함수로 표현하면 아래와 같다.
// 예제 24-13

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
  • 위 예제 24-13의 num(①)은 생성자 함수 Counter가 생성할 인스턴스의 프로퍼티가 아니라 즉시 실행 함수 내에서 선언된 변수다.
  • 만약 num이 생성자 함수 Counter가 생성할 인스턴스의 프로퍼티라면(②) 인스턴스를 통해 외부에서 접근이 자유로운 public 프로퍼티가 된다.
  • 하지만 즉시 실행 함수 내에서 선언된 num 변수는 인스턴스를 통해 접근할 수 없으며, 즉시 실행 함수 외부에서도 접근할 수 없는 은닉된 변수이다.
  • 생성자 함수 Counter은 프로토타입을 통해 increase, decrease 메서드를 상속받는 인스턴스를 생성한다.
  • increase, decrease 메서드는 모두 자신의 함수 정의가 평가되어 함수 객체가 될 때 실행 중인 실행 컨텍스트인 즉시 실행 함수 실행 컨텍스트의 렉시컬 환경을 기억하는 클로저이다.
  • 따라서 프로토타입을 통해 상속되는 프로토타입 메서드일지라도 즉시 실행 함수의 자유 변수 num을 참조할 수 있다.
  • 즉, num 변수의 값은 increase, decrease 메서드만이 변경할 수 있다.
  • 변수 값은 누군가에 의해 언제든지 변경될 수 있어 오류 발생의 근본적 원인이 될 수 있다.
  • 외부 상태 변경이나 가변 데이터를 피하고 불변성을 지향하는 함수형 프로그래밍에서 부수 효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.
// 예제 24-24

// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 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 함수는 보조 함수를 인자로 전달받고 함수를 반환하는 고차 함수이다.
  • makeCounter 함수가 반환하는 함수는 자신이 생성됐을 떄의 렉시컬 환경인 makeCounter 함수의 스코프에 속한 counter 변수를 기억하는 클로저이다.
  • makeCounter 함수는 인자로 전달받은 보조 함수를 합성하여 자신이 반환하는 함수의 동작을 변경할 수 있다.
  • 이때 주의해야 할 점은 makeCounter 함수를 호출해 함수를 반환할 때 반환된 함수는 자신만의 독립적인 렉시컬 환경을 갖는다는 것이다.
  • 이는 함수를 호출하면 그때마다 새로운 makeCounter 함수 실행 컨텍스트의 렉시컬 환경이 생성되기 때문이다.
  • ①과 ②애서 makeCounter 함수를 호출하면 makeCounter 함수의 실행 컨텍스트가 생성된다.
  • 그리고 makeCounter 함수는 함수 객체를 생성하여 반환한 후 소멸된다.
  • makeCounter 함수가 반환한 함수는 makeCounter 함수의 렉시컬 환경을 상위 스코프로서 기억하는 클로저이며, 전역 변수인 increase에 할당된다.
  • 이때 makeCounter 함수의 실행 컨텍스트는 소멸되지만 makeCounter 함수 실행 컨텍스트의 렉시컬 환경은 makeCounter 함수가 반환한 함수의 [[Environment]] 내부 슬롯에 의해 참조되고 있기 때문에 소멸되지 않는다.

  • 위 예제에서는 각각 자신만의 독립된 렉시컬 환경을 갖기 때문에 카운트를 유지하기 위한 자유 변수 counter을 공유하지 않아 카운터의 증감이 연동되지 않는다.
  • 따라서 독립된 카운터가 아니라 연동하여 증감이 가능한 카운터를 만들려면 렉시컬 환경을 공유하는 클로저를 만들어야 한다.
  • 이를 위해서는 makeCounter 함수를 두 번 호출하지 말아야 한다.
// 예제 24-15

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

캡슐화와 정보 은닉

캡슐화

  • 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것
  • 캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉 이라 한다.

정보은닉

  • 정보 은닉은 외부에 공개할 필요가 없는 구현의 일부를 외부에 공개되지 않도록 감추어 적절치 못한 접근으로부터 객체의 상태가 변경되는 것을 방지해 정보를 보호하고, 객체 간의 상호 의존성, 즉, 결합도를 낮추는 효과가 있다.
  • 대부분의 객체지향 프로그래밍 언어는 클래스를 정의하고 그 클래스를 구성하는 멤버(프로퍼티와 메서드)에 대하여 public, private, protected와 같은 접근 제한자를 선언하여 공개 범위를 한정할 수 있다.
  • 하지만 자바스크립트는 접근 제한자를 제공하지 않는다.
  • 따라서 자바스크립트 객체의 모든 프로퍼티와 메서드는 기본적으로 외부에 공개되어 있다. (= 객체의 모든 프로퍼티와 메서드는 기본적으로 public하다.)
// 예제 24-16

function Person(name, age) {
  this.name = name; // public
  let _age = age; // private

  // 인스턴스 메서드
  this.sayHi = function () {
    console.log(`Hi! My name is ${this.name}. I am ${_age}`);
  };
}

const me = new Person("Lee", 20);
me.sayHi(); // Hi! My name is Lee. I am 20
console.log(me.name); // Lee
console.log(me._age); // undefined

const you = new Person("Kim", 30);
you.sayHi(); // Hi! My name is Kim. I am 30
console.log(you.name); // Kim
console.log(you._age); // undefined 
  • 위 예제에서 name 프로퍼티는 외부로 공개되어 있어 자유롭게 참조하거나 변경할 수 있다. ( name은 public)
  • 하지만 _age 변수는 Person 생성자 함수의 지역 변수이므로 Person 생성자 함수 외부에서 참조하거나 변경할 수 없다. (_age는 private)
  • 하지만 sayHi 메서드는 인스턴스 메서드이므로 Person 객체가 생성될 때마다 중복 생성된다.
// 예제 24-17

function Person(name, age) {
  this.name = name; // public
  let _age = age; // private
}
// 인스턴스 메서드
Person.prototype.sayHi = function () {
  // Person 생성자 함수의 지역 변수 _age를 참조할 수 없다.
  console.log(`Hi! My name is ${this.name}. I am ${_age}`);
};
  • sayHi 메서드의 중복 생성을 방지하기 위해 위와 같이 코드를 작성하면
  • Person 생성자 함수의 지역 변수인 _age를 Person.prototype.sayHi 메서드 안에서 참조할 수 없는 문제가 발생한다.
// 예제 24-18

const Person = (function () {
  let _age = 0; // private

  // 생성자 함수
  function Person(name, age) {
    this.name = name; // public
    _age = age; // private
  }
  // 프로토타입 메서드
  Person.prototype.sayHi = function () {
    // Person 생성자 함수의 지역 변수 _age를 참조할 수 없다.
    console.log(`Hi! My name is ${this.name}. I am ${_age}`);
  };

  return Person;
})();

const me = new Person("Lee", 20);
me.sayHi(); // Hi! My name is Lee. I am 20
console.log(me.name); // Lee
console.log(me._age); // undefined

const you = new Person("Kim", 30);
you.sayHi(); // Hi! My name is Kim. I am 30
console.log(you.name); // Kim
console.log(you._age); // undefined
  • 위 예제 24-17에서는 즉시 실행 함수를 사용하여 Person 생성자 함수와 Person.prototype.sayHi 메서드를 하나의 함수 내에 모았다.
  • 위 패턴을 사용하면 접근 제한자를 제공하지 않는 자바스크립트에서도 정보 은닉이 가능한 것처럼 보인다.
  • Person 생성자 함수와 Person 생성자 함수의 인스턴스가 상속받아 호출할 Person.proototype.sayHi 메서드는 즉시 실행 함수가 종료된 이후 호출된다.
  • 하지만 Person 생성자 함수와 sayHi 메서드는 이미 종료되어 소멸한 즉시 실행 함수의 지역 변수 _age를 참조할 수 있는 클로저이다.
profile

DEVELOP

@JUNGY00N