fffo

클로저 본문

Programming/Javascript

클로저

gggs 2021. 10. 11. 23:02

클로저

렉시컬 스코프

  • js는 렉시컬 스코프를 따르는 프로그래밍 언어임
  • 렉시컬 스코프는 상위 스코프 결정 방식
    • js 에서는 변수를 현재의 렉시컬 스코프 내에서 찾지 못하면 상위 스코프로 이동하며 찾음
    • 이 때 상위 스코프는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정됨

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

  • 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경(위치), 즉 상위 스코프의 참조를 저장함
  • 저장하는 시기는 함수 정의가 평가되는 시점에 저장
    • 함수 정의가 평가되는 시점은 해당 코드가 평가되는 시점
    • 코드가 평가 될 때 함수 객체가 생성되고, 해당 함수 객체의 내부 슬롯 [[Environment]]에 현재 실행되고 있는 콜스택의 렉시컬 환경의 참조를 넣음
    • 추후에 함수가 실행되어 함수의 실행 컨텍스트를 생성하고 렉시컬 환경을 콜스택에 올릴 때 렉시컬 환경의 외부 렉시컬환경 참조로 해당 함수가 가지고 있던 내부 슬롯 [[Environment]]에 있던 참조 값을 넣음

클로저와 렉시컬 환경

  • 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있음. 이러한 중첩 함수를 클로저(closure)라고 부름
  • 모든 함수는 클로저이지만 일반적으로 클로저라 불리는 함수는 다음과 같은 특징을 가짐
    • 외부 함수보다 더 오래 살아있음
    • 외부 함수의 변수를 참조함
  • 클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수(free variable)라고 부름
  • 모던 js엔진은 클로저가 참조하고 있지 않는 식별자는 기억하지 않는 최적화를 해주므로 클로저로 인한 불필요한 메모리 낭비는 없음

클로저의 활용

  • 상태(state)를 안전하게 변경하고 유지하기 위해 클로저를 사용함
  • 상태를 안전하게 은닉(information hiding)하고 특정 함수에게만 상태 변경을 허용
function makeCounter(predicate) {
  let counter = 0;

  return function () {
    counter = predicate(counter);
    return counter;
  };
}

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

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

const increaser = makeCounter(increase);
console.log(increaser());
console.log(increaser());

const decreaser = makeCounter(decrease);
console.log(decreaser());
console.log(decreaser());
  1. 전역 평가
    1. 전역 객체 생성, 전역 실행 컨텍스트 생성, 전역 렉시컬 환경 생성 후 바인딩, 콜 스택에 추가
    2. 전역 환경 레코드 생성 및 전역 렉시컬 환경에 바인딩
    3. 전역 환경 레코드에 객체 환경 레코드와 선언적 환경 레코드 바인딩
    4. 객체 환경 레코드에 전역 객체 바인딩 후 전역 객체에 makeCounter, increase, decrease 함수 객체 생성 및 바인딩
      1. 함수 객체 생성 시 내부 슬롯 [[Environment]]에 현재 실행 컨텍스트인 전역 렉시컬 환경 바인딩
    5. 선언적 환경 레코드에 increaser, decreaser 변수를 프로퍼티로 선언
  2. 전역 실행
    1. increaser 변수에 makeCounter(increase) 함수 반환 값을 할당하기 위해 makeCounter 함수 실행
    2. 함수 평가 (makeCounter)
      1. makeCounter함수의 실행 컨텍스트 생성, 함수 렉시컬 환경 생성 후 바인딩, 콜 스택에 추가
      2. 함수 환경 레코드 생성 및 함수 렉시컬 환경에 바인딩 후 인자로 전달 받은 increase 함수 객체를 매개변수 predicate에 할당, counter 변수를 프로퍼티로 선언
      3. this바인딩은 makeCounter함수가 일반 함수로 호출되었으므로 전역 객체 바인딩
      4. 외부 렉시컬 환경 참조는 makeCounter함수 객체가 생성될 때 내부 슬롯 [[Environment]]에 바인딩 되었던 전역 렉시컬 환경 바인딩
    3. 함수 실행 (makeCounter)
      1. counter에 0을 할당
      2. 리턴 값으로 사용되는 함수 객체 생성 및 리턴
        1. 이 때 생성되는 함수 객체의 내부 슬롯 [[Environment]]는 현재 실행 컨텍스트인 makeCounter의 렉시컬 환경을 바인딩
    4. 함수 종료 후 콜 스택에서 pop
    5. 리턴된 함수 객체를 increaser에 바인딩
    6. (console을 찾는 스코프체인과 log를 찾는 프로토타입체인 과정 생략)
    7. increaser함수 실행
    8. 함수 평가
      1. increaser함수의 실행 컨텍스트 생성, 함수 렉시컬 환경 생성 후 바인딩, 콜 스택에 추가
      2. 함수 환경 레코드 생성 및 함수 렉시컬 환경에 바인딩
      3. this바인딩은 함수가 일반 함수로 호출되었으므로 전역 객체 바인딩
      4. 외부 렉시컬 환경 참조는 increaser함수 객체가 생성될 때 내부 슬롯 [[Environment]]에 바인딩 되었던 makeCounter의 렉시컬 환경 바인딩
    9. 함수 실행
      1. counter 변수가 현재 함수 환경 레코드에 없으므로 외부 렉시컬 환경 참조로 바인딩된 makeCounter의 렉시컬 환경으로 가서 찾기
      2. makeCounter의 렉시컬 환경의 환경 레코드에 있는 counter변수에 predicate함수의 반환값을 넣기 위해 predicate 함수를 동일한 방법으로 찾아서 실행
      3. ... 생략 ...

캡슐화와 정보 은닉

  • 클로저를 활용해 타 언어의 public, private, protected 같은 접근 제한자 흉내를 낼 수 있음
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}, ${_age} years old`);
  };
  return Person
}());

const p1 = new Person('dlwlrma', 29); 
const p2 = new Person('oilnam', 81); 
p1.sayHi(); // Hi! I'm dlwlrma, 81 years old
p2.sayHi(); // Hi! I'm oilnam, 81 years old
  • 그러나 위와 같이 인스턴스 메서드 대신 프로토타입 메서드를 사용하면 프로토타입 메서드는 한 번만 정의되기 때문에 클로저의 렉시컬 환경도 하나가 됨
  • private 타입은 후에 배울 새로운 표준 사양으로 구현됨

'Programming > Javascript' 카테고리의 다른 글

클래스 상속  (0) 2021.10.15
클래스  (0) 2021.10.13
실행 컨텍스트  (0) 2021.10.07
this  (0) 2021.10.06
빌트인 객체  (0) 2021.10.05
Comments