코드노트

자바스크립트 클로저(Closure) 정리 본문

Code note/자바스크립트

자바스크립트 클로저(Closure) 정리

코드노트 2022. 11. 14. 15:27

클로저(Closure)

- 함수와 함수가 선언된 어휘적 환경(렉시컬 환경)의 조합

- 외부 함수보다 충첩함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명주기가 종료한 외부 함수의 변수를 참조

- 중첩 함수 == 클로저

 

-> 내부함수가 외부함수 변수에 접근 가능

function init() {
	// name은 init에 의해 생성된 지역 변수
	var name = "Mozilla";	 
 
	// displayName() 은 내부 함수이며, 클로저
	function displayName() {  
		 // 부모 함수에서 선언된 변수를 사용
		alert(name);	
	}
	displayName();
}
init();

쉽게 이야기하자면 함수안에 함수를 만들었을때 그 외부에 있는 변수를 사용할 수 있다.

- 부모 함수에서 선언된 변수를 사용하는 것!

 

function makeFunc() {
	var name = "Mozilla";
	function displayName() {
		alert(name);
	}
	return displayName;
}

var myFunc = makeFunc();
//myFunc변수에 displayName을 리턴함
//유효범위의 어휘적 환경을 유지
myFunc();
//리턴된 displayName 함수를 실행(name 변수에 접근)

- 외부함수의 환경을 기억한다.

- 클로저는 반환된 내부함수가 자신이 선언되었을 때 환경의 스코프를 기억하여

자신이 선언되었을 때 환경밖에서 호출되어도 그 환경을 접근할 수 있는 함수를 의미한다.

 

 

* 이 같은 현상이 발생하는 이유는 자바스크립트가 렉시컬 스코프를 따르는 언어이기 때문

- 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프라 한다.

const x = 1;

function a() {
  const x = 10;
  b();
}

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

a(); // 1
b(); // 1

- 두 함수 모두 전역에서 정의된 전역 함수이다.

- 함수의 상위 스코프는 함수를 어디서 정의했느냐에 따라 결정되기 때문에 a, b함수는 상위 스코프는 전역이다.

- 함수를 어디서 호출하는지는 함수의 상위 스코프 결정에 어떠한 영향도 주지 못한다.

- 함수의 상위 스코프는 함수를 정의한 위치에 의해 정적으로 결정되고 변하지 않는다.

 

* 상위 스코프는 함수 정의 환경(위치)에 따라서 결정

* 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.


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

- 함수는 자신의 내부슬롯 [[ Environment ]] 에 상위 스코프의 참조를 저장

* 현재 실행중인 실행 컨텍스트의 렉시컬 환경을 가리킨다.


- 클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첨 함수가 외부 함수보다 더 오래 유지되는 경우 한정하는것이 일반적.

- 클로저가 참조하고 있지 않은 식별자는 기억하지 않는다. 


자유변수

- 클로저에 의해 참조되는 상위 스코프의 변수

 


클로저 활용 예

- 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용

- 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용

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

const increase = function () { // 카운트 상태 변경 함수
  return ++num; // 카운트 1 증가
};

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


// (2)
let increase = (function () { // 카운트 상태 변경 함수
  let num = 0; // 카운트 상태 변수
  return function () { // 클로저
    return ++num; // 카운트 1 증가
  };
})();

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

- 같은 값을 호출하는 코드이지만 클로저를 통해서 변수 값을 오류가 나지 않게 할 수 있다.

 

(1) 첫번째 코드

- 전역변수로 지정되어 있기 때문에 접근도 쉽고 오류 가능성이 높다.

 

(2) 두번째 코드

- 즉시 실행 함수를 통해서 변수에 할당된 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프를 기억하는 클로저 이다.

- 즉시 실행 함수는 호출된 이후 소멸되지만 즉시 실행 함수가 반환한 클로저는 변수에 할당되어 호출된다.

- 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프(함수의 렉시컬 환경)를 기억하고 있다.

- 카운트 상태를 유지하기 위한 자유 변수 num을 언제 어디서 호출하던지 참조 가능


const counter = (function () {
  let num = 0;
  
  // 클로저인 메서드를 갖는 객체를 반환
  // 객체 리터럴은 스코프를 만들지 않는다.
  // 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경
  return {
    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

- increase, decrease 메서드의 상위 스코프는 메서드가 평가되는 시점에 실행중인 즉시실행 함수 실행컨텍스트의 렉시컬 환경

- 언제 어디서 호출되든 상관없이 즉시 실행 함수의 스코프의 식별자를 참조 가능

 

- 위 예제를 생성자 함수로 표현하면?

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

- 즉시실행함수로 내에 선언된 변수는 인스턴스를 통해 접근할 수 없다.

- 프로토타입을 통해 메서드를 상속받는 인스턴스를 생성

- num변수의 값은 메서드만이 변경할 수 있다.

 

* 변수 값은 누구나 변경이 가능하면 오류 발생의 원이이 된다.

* 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용


- 고차함수 예시

// 함수를 인수로 전달 받고 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환
function makeCounter(a) {
// 카운터 상태를 유지하기 위한 자유 변수
  let counter = 0;

// 클로저 반환
  return function () {
    counter = a(counter);
    return counter;
  };
}

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

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

// 두 함수는 자신만의 독립된 렉시컬 환경을 갖기 때문에 카운트 유지가 안된다.
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2

- 어떻게 보면 당연하다 카운트 상태를 유지하려면 렉시컬 환경을 공유하는 클로저를 만들어야 한다.

- makeCounter()함수를 두번 호출하면 안된다.


- 그럼 어떻게?

- 렉시컬 환경을 공유하는 클로저를 만들자!

const counter = (function () {
  let counter = 0;
  return function (a) {
    counter = a(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