본문 바로가기
[Study] Deep Dive 스터디

[JS] 클로저

by 지공A 2024. 1. 14.

24장 : 클로저

MDN에서 정의된 클로저란?

클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
const x = 1;
function outerFunc() {
  const x = 10;
  innerFunc();
}
function innerFunc() {
  console.log(x); // 1
}

outerFunc();​
  • innerFunc 함수를 outerFunc 함수의 내부에서 호출한다 하더라도 outerFunc 함수의 변수에 접근할 수 없다
  • 자바스크립트는 렉시컬 스코프를 따르는 프로그래밍 언어이기 때문이다

 

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

  • 자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디서 정의했는지에 따라 상위 스코프를 결정한다.
  • 함수를 어디서 호출하는지는 상위 스코프 결정에 어떠한 영향도 주지 못한다.
  • 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다. 이것이 바로 렉시컬 스코프이다.

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

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

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

// bar 는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 [[Environment]]에 저장하여 기억한다
function bar() {
  console.log(x);
}

foo();
bar();

3. 클로저와 렉시컬 환경

  • 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저(clousure)라고 부른다.
const x = 1;

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

// outer 호출 시 중첩 함수 inner 를 return
// 그리고 outer 의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거된다.
const innerFunc = outer();
innerFunc(); // 10
  • 중첩 함수 inner 를 반환하고 생명 주기를 마감한 outer 함수의 변수 x 의 값을 어떻게 참조할 수 있을까?
  • 외부 함수보다 더 오래 생존한 중첩 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프를 기억하기 때문에, 상위 스코프를 참조할 수 있기 때문이다!
  • 따라서 inner 함수는 상위 스코프 outer 함수의 변수 x 의 값을 참조할 수 있다.

4. 클로저의 활용

  • 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.
  • 즉, 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.
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
  • increaser 와 decreaser 는 각각 makeCounter 함수 호출 시 생성된 자신만의 독립된 렉시컬 환경을 갖는다.

5. 캡슐화와 정보 은닉

  • 캡슐화(encapsulation) : 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것
  • 캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉이라 한다.
  • 자바스크립트는 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
  • name 프로퍼티는 외부로 공개되어 있어 자유롭게 참조하거나 변경할 수 있어 public 하다.
  • _age 변수는 Person 생성자 함수의 지역 변수이므로 외부 함수에서 참조하거나 변경할 수 없어 private 하다.

6. ⭐클로저 사용 시 let과 var⭐

1. var : 함수 스코프

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
}
  • var 로 선언된 i 변수는 함수 스코프를 가지기 때문에 모든 루프에서 같은 스코프를 공유한다.
  • for 문을 돌면서 funcs 배열에는 각 익명 함수가 할당되고, 전역 변수 i는 for 문을 돌면서 3 이 된다. 
    • ex ) 
    • func[0] = function () 
    • func[1] = function () 
    • ...
  • for 문 종료 후 각 함수들이 실행되는데, 여기서 이미 전역 변수 i는 3이므로 각 배열의 값은 모두 3이 출력된다.

2. let : 블록 스코프

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
}
  • let 으로 선언된 i 변수는 블록 스코프를 가지기 때문에 각 루프에서 새로운 스코프를 생성하고 그 스코프는 해당 루프에서의 i 변수를 기억한다.
  • 배열에 저장된 각 함수는 함수가 생성된 시점에서의 독립적인 스코프를 가지고 있다.
  • 따라서 for 문 종료 후 각 함수들이 실행될 때의 i 값을 참조하기 때문에 각 배열들의 값은 0, 1, 2 가 된다!

Deep Dive Study week - 01

'[Study] Deep Dive 스터디' 카테고리의 다른 글

[JS] 이터러블  (0) 2024.01.23
[JS] 프로미스  (0) 2024.01.22
[JS] 비동기 프로그래밍  (0) 2024.01.20
[JS] ES6 함수의 추가 기능  (0) 2024.01.16
[JS] this  (1) 2024.01.13