티스토리 뷰

스터디 자료

24장 클로저

도토리줄기 2022. 10. 25. 21:25

1.렉시컬 스코프

JS의 특징 중 하나는, 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라서 상위 스코프를 결정하는 방법인 렉시컬 스코프(정적 스코프)를 따른다. 아래와 같이 작성한 코드에서 전역으로 선언한 x와 지역으로 선언한 x가 있다. 10,1이 출력될 것 같지만 함수 bar는 foo안에서 스코프가 결정된 것이 아닌, 선언된 위치인 전역쪽에서 스코프가 결정되었기 때문에  1,1이 출력된다.

const x=1;

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

2.함수 객체의 내부 슬롯

위와 같이 함수가 호출된 위치와 선언된 위치는 다를 수 있다. 이를 위해 함수는 자신의 상위 스코프를 항상 내부슬롯 [[Environment]]에 상위 스코프의 참조를 저장한다.

3.클로저와 렉시컬 환경

클로저란 이미 생명주기가 종료된 함수 내부의 변수의 생명주기가 더 오래 유지되는 현상이라고 할 수 있다. 아래와 같이 outer함수 안에 선언된 x의 생명주기는 outer선언이 종료된 이후 사용되지 않을 것 처럼 보이지만, inner의 상위 스코프는 outer의 렉시컬 환경으로 설정되어 있기 때문에 아래에서 새롭게 호출하여도 내부의 변수인 10이 호출되는 것이다.

const x = 1;

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

const innerFunc = outer();
innerFunc();	//10

아래와 같이 오래 유지되지만 상위 스코프의 어떤 식별자도 참조하지 않는 경우는 상위 스코프를 기억하지 않는다. 따라서 bar는 클로저가 아니다. 또한 함수 내부에서 먼저 생명 주기가 종료되는 경우에도 클로저라고 하지 않는다. 

<!DOCTYPE html>
<html>
<body>
  <script>
    function foo() {
      const x = 1;
      const y = 2;

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

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

      return bar;
    }

    const bar = foo();
    bar();
  </script>
</body>
</html>

4.클로저 활용

클로저는 상태를 안전하게 변경하고 유지하기 위해 사용되기 때문에  상태를 은닉하고 특정 함수에게만 상태를 변경을 허용하기 위해 사용한다. 아래의 코드는 외부에서 num의 값이 변경될 경우 오류가 생기기 때문에 좋은 코드가 아니다. 

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

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

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

아래 코드의 즉시실행 함수는 호출 된 이후 소멸하지만, 반환한 클로저는 increase변수에 할당되어 호출된다. 이때 클로저는 즉시 실행함수의 렉시컬 환경을 기억하고 있다. 따라서 즉시 실행 함수라 호출될 때 마다 초기화 되는 일 없이 num의 값을 유지하고 있을 수 있는 것이다. 

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

함수형 프로그래밍에서 클로저를 사용하는 경우이다. 반환하는 함수는 makecounter함수의 스코프에 속한 counter변수를 기억하는 클로저이다. 인자로 전달 받은 보조 함수를 합성하여 자신이 반환하는 함수의 동작을 변경 가능하다.  makecounter함수를 호출해 반환할 때 마다 자신만의 독립된 렉시컬 환경을 갖기 때문에 값이 유지되지는 않는다.

 

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

decrease와 increase가 같은 counter를 가지고 동작하게 하기 원한다면, 변수에 할당하지 않고 직접 호출한다면 같은 렉시컬 환경을 공유하여 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

5. 캡슐화와 정보 은닉

캡슐화는 객체의 상태를 나타내는 프로퍼티와 동작인 메서드를 하나로 묶는것을 의미한다. 캡슐화는 특정 프로퍼티나 메서드를 감추는 목적으로 사용하는데 이를 정보 은닉이라고 한다. 자바스크립트는 접근제한자(public,private등)을 이용하지 않기 때문에 기본적으로 public이다. 아래와 같이 Person으로 생성하게 되면 _age 변수는 생성자의 지역변수이므로 private로 사용할 수 있다.

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

아래의 코드와 같이  프로토타입 메서드를 사용하여 Person의 sayHi 메서드를 구현하게 되면, 정보 은닉이 가능한 것 처럼 보이게 사용할 수 있다. 하지만 sayHi메서드는 한번만 생성되는 클로저이기 때문에 _age의 변수를 유지하게 동작할 수 없다. JS는 정보 은닉을 완벽하게 지원하지 않지만, 21년1월 이후로 private를 정의할 수 있는 표준 사양이 제안되어 사용이 가능하다. 

 

6.클로저 관련 사용 실수

아래의 코드와 같이 즉시 실행함수를 사용하여 값을 할당하면, for문 안에 있는 변수 i는 함수 레벨 스코프를 갖기 때문에 모든 요소가 3을 참조하게 된다. 

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]()); // ②
}

이와 같은 실수를 막기 위해서는 var를 사용하는것을 되도록 지양하고 let을 사용하면 같은 코드라 하더라도 원하는 대로 출력값을 얻을 수 있다.

const funcs = [];

for (let i = 0; i < 3; i++) {
  funcs[i] = function () { return i; };
}

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

'스터디 자료' 카테고리의 다른 글

[JavaScript] 타이머  (0) 2022.12.06
브라우저의 렌더링 과정  (0) 2022.11.18
37장 Set과 Map  (0) 2022.11.18
26장. ES6 함수의 추가 기능  (0) 2022.11.01
12장 함수  (0) 2022.10.10
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함