[자바스크립트 Deep Dive] 24. 클로저

unhyif·2023년 3월 27일
0

24.1 렉시컬 스코프

함수 정의 위치에 의해 상위 스코프가 정적으로 결정되는 것

const x = 1;

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

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

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

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

  • 함수 정의가 평가되어 함수 객체가 생성될 때, 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경(상위 스코프의 참조)을 저장함

  • 이때 해당 참조는 현재 실행 중인 실행 컨텍스트의 렉시컬 환경을 가리킴
    => [[Environment]]에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경 = 상위 스코프

  • 함수는 존재하는 동안 내부 슬롯 [[Environment]]를 통해 자신의 상위 스코프를 기억함


함수 호출 시 함수 코드 평가 과정

  1. 함수 실행 컨텍스트 생성
  2. 함수 렉시컬 환경 생성
    2-1. 함수 환경 레코드 생성
    2-2. this 바인딩
    2-3. 외부 렉시컬 환경에 대한 참조 결정(스코프 체인 생성)

=> 외부 렉시컬 환경에 대한 참조에 [[Environment]]에 저장된 렉시컬 환경의 참조가 할당됨


24.3 클로저와 렉시컬 환경

클로저: 일반적으로 생명 주기가 종료된 외부 함수(상위 스코프)의 변수를 참조할 수 있는 중첩 함수를 의미함

이론적으로는, 상위 스코프의 식별자를 참조하거나 식별자의 값을 변경할 수 있는 함수를 의미함 => 모든 함수는 사실 클로저임


const x = 1;

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

const innerFunc = outer();
innerFunc(); // 10
  • outer 함수 실행 종료 시, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되어도 렉시컬 환경은 유지됨
    • outer 함수의 렉시컬 환경은 inner 함수의 [[Environment]]에 의해 참조되고 있고, inner 함수는 innerFunc 에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 아님
      cf) 가비지 컬렉터는 참조되고 있는 메모리 공간을 해제하지 않음

=> 외부 함수가 소멸해도 반환된 중첩 함수는 외부 함수의 변수를 참조할 수 있음


  • 상위 스코프의 어떤 식별자도 참조하지 않는 함수는 클로저로 보지 않고, 대부분의 모던 브라우저는 상위 스코프를 기억하지 않음
function foo() {
  const x = 1;

  // 일반적으로 클로저로 보지 않음
  function bar() {
    const y = 2;
    console.log(y);
  }

  return bar;
}

const bar = foo();
// 상위 스코프는 기억되지 않음
bar();
  • 외부 함수보다 생명주기가 짧은 중첩 함수는 클로저로 보지 않음
function foo() {
  const x = 1;

  // 클로저였지만 곧바로 소멸함
  function bar() {
    console.log(x);
  }

  bar();
}

foo();
  • 대부분의 모던 브라우저는 클로저가 참조하는 식별자만 기억함
function foo() {
  const x = 1;
  const y = 2;

  // 클로저
  function bar() {
    console.log(x);
  }

  return bar;
}

const bar = foo();
// foo의 y는 기억되지 않음
bar();

cf) 자유 변수: 클로저가 참조하는 상위 스코프의 변수


24.4 클로저의 활용

  • 상태를 안전하게 유지하고 변경하기 위해 클로저를 사용함
    = 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위함
const increase = (function () {
  let num = 0;
  return function () {
    return ++num;
  };
})();

console.log(increase()); // 1
console.log(increase()); // 2
const counter = (function () {
  let num = 0;

  return {
    // 클로저인 메소드를 갖는 객체를 반환함
    increase() {
      return ++num;
    },
    decrease() {
      return --num;
    },
  };
})();

console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0
  • 함수형 프로그래밍에서 부수 효과를 최대한 억제하고 프로그램의 안정성을 높이기 위해 클로저를 사용함
// 함수를 인수로 전달받고 함수를 반환하는 고차함수
function makeCounter(aux) {
  // 자유 변수
  let num = 0;

  return function () {
    num = aux(num);
    return num;
  };
}

function increase(n) {
  return ++n;
}
function decrease(n) {
  return --n;
}

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
function makeCounter() {
  let num = 0;

  return function (aux) {
    num = aux(num);
    return num;
  };
}

function increase(n) {
  return ++n;
}
function decrease(n) {
  return --n;
}

const counter = makeCounter();
// 같은 상위 스코프를 참조함 (자유 변수를 공유함)
console.log(counter(increase)); // 1
console.log(counter(decrease)); // 0

24.5 캡슐화와 정보 은닉

캡슐화: 객체의 프로퍼티와 메소드를 하나로 묶는 것

정보 은닉: 객체의 특정 프로퍼티나 메소드를 감출 목적으로 캡슐화하는 것

  • 구현의 일부를 비공개하여 객체의 상태 변경을 방지함으로써 정보를 보호하고, 객체 간의 결합도(coupling)를 낮춤

JS는 접근 제한자를 제공하지 않으므로 객체의 모든 프로퍼티와 메소드는 기본적으로 public함

  • 인스턴스 메소드를 활용한 정보 은닉
function Person(name, age) {
  this.name = name;
  const _age = age;

  // 인스턴스 메소드
  this.sayHi = function () {
    console.log(`Hi, I'm ${this.name} and ${_age}`);
  };
}

const me = new Person('J', 20);
me.sayHi(); // "Hi, I'm J and 20"
console.log(me.name); // 'J', public
console.log(me._age); // undefined, private
  • 일반 프로토타입 메소드를 활용한 정보 은닉 (불가능)
function Person(name, age) {
  this.name = name;
  const _age = age;
}

// 프로토타입 메소드
Person.protoType.sayHi = function () {
  // Person 생성자 함수의 지역 변수 _age를 참조할 수 없음
  console.log(`Hi, I'm ${this.name} and ${_age}`);
};
  • 클로저인 프로토타입 메소드를 활용한 정보 은닉 (불완전)
const Person = (function () {
  let _age = 0;

  // 클로저
  function Person(name, age) {
    this.name = name;
    _age = age;
  }
  // 클로저
  Person.prototype.sayHi = function () {
    console.log(`Hi, I'm ${this.name} and ${_age}`);
  };

  return Person;
})();

const me = new Person('J', 20);
me.sayHi(); // "Hi, I'm J and 20"
const you = new Person('H', 30);
you.sayHi(); // "Hi, I'm H and 30"
// _age 값이 유지되지 않음
me.sayHi(); // "Hi, I'm J and 30"

=> JS는 정보 은닉을 완전하게 지원하지 않음. 최신 브라우저에서는 클래스에 private 필드를 정의할 수 있음


24.6 자주 발생하는 실수

const funcs = [];

// 코드 블록이 반복 실행될 때마다 새로운 렉시컬 환경이 생성되어 식별자 값이 유지됨
for (let i = 0; i < 3; i++) {
  funcs[i] = function () {
    return i;
  };
}

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

// 배열에 추가된 함수들은 클로저임
const funcs = Array.from(new Array(3), (_, i) => () => i);
funcs.forEach(f => console.log(f())); // 0 1 2

0개의 댓글