모던 자바스크립트 Deep Dive : 24장 클로저

EdLee·2022년 11월 13일

javascript

목록 보기
14/37

24장 클로저

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

1. 렉시컬 스코프(정적 스코프)


  • 자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다.
const x = 1;

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

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

foo(); // 1
bar(); // 1
  • foo() 안에서 bar()를 호출하더라도, bar가 바라보는 x는 전역의 x이다.
  • bar() 어디에서 호출했건, bar()는 전역에서 선언됐기 때문에 상위 스코프는 전역이다.

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


  • 함수가 정의된 환경과 호출되는 환경은 다를 수 있다.

  • 그런데 항상 자신이 정의된 환경을 기억한다는 것은, 함수 어딘가에 이것이 기록되어 있다는 의미이다.

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

  • 다시 한 번 함수 코드 평가 순서를 살펴보면,

  1. 함수 정의 평가 => 실행 컨텍스트의 렉시컬 환경인 전역 렉시컬 환경의 참조가 [[Enviornment]]에 저장
  2. 함수 실행 컨텍스트 생성
  3. 함수 렉시컬 환경 생성
    2.1. 함수 환경 레코드 생성
    2.2. this 바인딩
    2.3. 외부 렉시컬 환경에 대한 참조 결정 => 함수 객체의 내부 슬롯 [[Environment]]에 저장된 렉시컬 환경의 참조가 할당됨

3. 클로저와 렉시컬 환경


const x = 1;

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

const innerFunc = outer(); // outer()의 실행 종료와 함께, 지역 변수 x와 inner는 소멸했을 것 같지만,
innerFunc(); // 10
  • 외부 함수 보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다.
  • 이러한 중첩 함수를 클로저라고 부른다.

잊지 말 것! 실행 컨텍스트와 렉시컬 환경은 별개의 객체로 관리된다. 함수 종료와 함께 실행 컨텍스트는 스택에서 소멸하지만, 렉시컬 환경은 가비지 컬렉션에 의해 계속 관리된다.

4. 클로저의 활용


  • 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.
    => 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.

  • 예시 : 함수 호출 횟수를 누적해 출력하는 카운터를 보고 이해해보자.

잘못된 코드 예시 1

let num = 0;

const increase = function () {
  return ++num;
}

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
  • 위 코드는 동작은 잘 하지만, num이 전역 변수인 만큼 누가 언제 어디서 변경할 지 모르는 위험한 구조이다.

잘못된 코드 예시 2

const increase = function () {
  let num = 0;  
  return ++num;
};

console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
  • num을 지역 변수로 바꿔서 아무나 num을 바꿀 수 없게 했다.
  • 하지만, 호출 때마다 num은 0으로 초기화되기 때문에 제 기능을 하지 못한다.

클로저 활용 예시

const increase = (function() {
  let num = 0;
  return function() { // 클로저를 반환
    return ++num;
  };
}()); // 즉시 실행 함수 자체는 여기서 실행이 끝났다. increase에는 중첩함수가 담겼다.

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

5. 캡슐화와 정보 은닉


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

...그런데 😥

  • 보통 객체 지향 프로그래밍 언어는 클래스에 public, private, protected와 같은 접근 제한자를 제공하지만, js에는 그런거 없다.
  • 🙄어쩌지?

JS에서 정보 은닉 흉내내기

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

  // 생성자 함수
  function Person(name, age) {
    this.name = name; // public
    _age = age;
  }

  // 프로토타입 메서드
  Person.prototype.sayHi = function() { // sayHi 메서드의 중복 생성을 막기 위해 prototype 메서드로 선언
    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

// 그런데...
// _age 변수 값이 변경된다!
me.sayHi(); // Hi! My name is Lee. I am 30.
  • 만약 Person 생성자 함수로 여러 개의 인스턴스를 만든다면, _age 변수의 상태가 유지되지 않는다.
  • Person.prototype.sayHi 메서드는 단 한 번 생성되어 _age 하나만을 돌려쓰기 때문..
  • 그렇다고 sayHi의 중복 생성을 놔두기엔, 상속 개념을 훼손한다.

6. 자주하는 실수


클로저 사용 전

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 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]()); // 0, 1, 2
}
  • 근데 var 썼잖아

var 없이

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
}
  • 그리고 클로저도 없이...
  • 위처럼 클로저가 필요없을 때도 있다.

0개의 댓글