24장. 클로저

Happhee·2022년 1월 17일
0

JS : Depp Dive

목록 보기
21/35
post-thumbnail

클로저는 내부 함수로부터 외부함수에의 접근 권한을 준다
클로저는 함수 생성 시점에 언제나 생긴다

const x = 1;

function outerFunc() {
  const x = 10;

  function innerFunc() {
    console.log(x); // 10
  }	//innerFunc와 outerFunc 사이의 closure

  innerFunc();
} // outerFunc와 전역컨텍스트 사이의 closure

outerFunc();

1. 렉시컬 스코프

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

const x = 1;

const x = 1;

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

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

foo(); // ? -> 1
bar(); // ? -> 1

foo와 bar 함수의 상위 스코프는 전역이다. 그러므로, 상위 스코프에 대한 참조값이 전역 컨텍스트에서 결정되어서 foo(), bar()의 값이 모두 1로 평가된다.

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

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

3. 클로저와 렉시컬 함수

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함수는 중첩 함수 inner를 반환하면서 생명주기가 종료된다.
즉, outer함수의 실행이 종료되면 outer함수의 실행 컨텍스트가 실행 컨텍스트 스택에서 제거되어 더이상 outer함수의 지역변수 x는 유효하지 않다

그런데 왜 10이 나오느냐?
outer함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만, outer함수의 렉시컬 환경까지 소멸하는 것은 아니다

innerFunc가 받는 것은 inner [[Environment]] outer L/E이므로
inner의 상위스코프인 outer의 x를 참조하여 반환한다.

이처럼 외부 함수보다 중첩함수가 더 오래 유지되면,
중첩 함수는 이미 생명 주기가 종료된 외부 함수의 변수를 참조 할 수 있으며
이러한 중첩 함수클로저라고 한다

  • 주의!
    자바스크립트의 모든 함수는 상위 스코프를 기억하기에 이론적으로는 모든 함수는 클로저로 볼 수 있지만, 일반적으로는 모든 함수를 클로저라고 하지 않는다
<!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>
 <!DOCTYPE html>
<html>
<body>
  <script>
    function foo() {
      const x = 1;

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

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

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

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

위의 예제의 경우 클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수라고 한다.
즉, 클로저란 함수가 자유변수에 대해 닫혀있다라는 의미이다.
또는 자유 변수에 묶여있는 함수이다

또한, 클로저는 상위 스코프를 기억해야 하기에 불필요한 메모리 점유를 걱정할 수도 있겠지만, 사실 클로저의 메모리 점유는 필요한것을 기억하기 위함이므로 딱히 걱정할 대상은 아니기에 필요하면 적극적으로 활용하는 것이 좋다

4. 클로저의 활용

그렇다면 어떻게 클로저를 활용할까,,,?

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

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

  // 클로저
  return function () {
    // 카운트 상태를 1만큼 증가 시킨다.
    return ++num;
  };
}());

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

이처럼 클로저는 상태가 의도하지 않게 변경되는 것을 막기 위해 안전하게 은닉하고, 특정 함수에게만 상태 변경을 허용하여 상태를 관리한다

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

생성자 함수로써 구축

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

5. 캡슐화와 정보 은닉

대부분의 객체 지향 프로그래밍 언어는 클래스를 정의하고, 그 클래스를 구성하는 멤버에 대하여 public, private, protected 같은 접근제한자를 사용하여 공개 범위를 한정하고 있지만, 자바스크립트는 접근제한자가 제공되지 않는다
즉, 자바스크립트 객체의 모든 프로퍼티와 메서드는 기본적으로 외부에 공개되어 있다

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

즉시 실행 함수 및 프로토 타입 메서드를 가진 생성자 함수를 반환

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

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

  // 프로토타입 메서드
  Person.prototype.sayHi = function () {
    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.
me.sayHi(); 	// Hi! My name is Lee. I am 30. 
//하나의 생성자만 형성하기에 age가 바뀜
console.log(you.name); // Kim
console.log(you._age); // undefined

바뀐 문법으로 적용한 클로저 함수

class Person {
	#age;
    constructor(name, age){
    	this.name = name;
      	this.#age = age;
    }
	sayHi(){
    	 console.log(`Hi! My name is ${this.name}. I am ${this.#age}.`);
    }
}
const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20.
console.log(me.name); // Lee
me.#age // SyntaxError -> Private 

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

전역 컨텍스트 i를 참조하고 있기에!!
이를 블록 스코프 변수로 바꿔주면 해결이 된다

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
}
profile
즐기면서 정확하게 나아가는 웹프론트엔드 개발자 https://happhee-dev.tistory.com/ 로 이전하였습니다

0개의 댓글