스터디 6회차 주간 공부 내용 - JS 클로저

잔잔바리디자이너·2022년 4월 4일
0

Study

목록 보기
6/19
post-thumbnail

클로저

클로저는 자바스크립트를 배워오면서 계속 듣던 용어였는데 여태 애써 외면해왔다ㅎ 왜냐면 알고싶지 않았거든요.

클로저의 정의

함수와 그 함수가 선언된 렉시컬 환경과의 조합

이러면 엥? 뭔소리야 이러고 10번 다시 읽어야됨. 자 이제 저게 뭔 말인지 알아보자~
기억 나니? js 엔진은 함수를 어디서 정의했는지에 따라 상위 스코프를 결정한다고.

const x = 1;
function foo(){
  const x = 10;
  bar();
}
function bar(){
  console.log(x);
}
foo(); // ? 뭘까
bar(); // ? 뭘까

실행 컨텍스트에서 살펴보았듯이 함수의 상위 스코프를 결정한다는 것은 렉시컬 환경의 외부 렉시컬 환경에 대한 참조 값을 결정한다는것과 같다. 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경의 참조를 저장한다. 즉 실행중이던 실행 컨텍스트의 렉시컬 환경의 참조를 저장한다. 때문의 위의 코드에서 foo, bar의 함수는 정의되던 시점에 전역 렉시컬 환경의 참조를 저장할것이다. 따라서 전역 스코프에서 찾은 x의 값 1을 뱉는다.

됐고 그래서 클로저가 뭐라는거야

const x = 1;
function outer(){
  let x = 10;
  const inner = function(){
    x = 20;
    console.log(x)
  }
  return inner
}
const innerFunction = outer();
console.log(innerFunction()); // 20

위의 코드를 보면 outer 함수는 inner 함수를 뱉어내고 생명 주기를 마감한다. 즉 실행 컨텍스트 스택에서 pop된다. outer 함수가 사망해버렸기 때문에 함수의 지역 변수 x도 생명 주기를 마감친다. 즉 지역 변수 x를 참조할 길이 없다는 말이시다~ 그러나...inner함수를 호출했더니 마감한줄 알았던 x가 돌아와버렸다.

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

헤엑 이게 가능한 이유가 뭐야?

결론적으로는 외부 함수의 생명 주기가 종료되어 외부 함수의 실행 컨텍스트가 스택에서 제거 되더라도, 외부 함수의 렉시컬 환경까지 소멸하는 것은 아니기 때문이다. 중첩 함수의 [[Environment]] 내부 슬롯에 의해 외부 함수의 렉시컬 환경이 참조되고 있고 중첩 함수는 innerFunction에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 되지 않는다.

그럼 모든 중첩 함수는 클로저야?

그건 아니고~

  1. 상위 스코프의 식별자를 참조하는 함수여야 한다. 모던 브라우저는 똑똑하기 때문에 쓸데없이 참조하지도 않는 식별자들을 기억하지 않는단다.
  2. 상위 스코프의 식별자를 참조 하더라도 외부 함수보다 내부 함수의 생명 주기가 길어야 한다.

자 요약하자면 클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고, 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한해서 부른다.

그래서 클로저, 어떻게 활용하지?

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

만약 카운트를 세어주는 함수가 필요하다고 했을 때, 아래처럼 구현하는것은 예상대로 동작하지 않거나 좋지 않다.

// 호출할 때 마다 함수의 새로운 렉시컬 환경을 만들어 내며 
// 변수를 재선언 하기 때문에 평생 0임;;
const increase = function(){
  let num = 0;
  return ++num;
}
increase(); // 1
increase(); // 1
increase(); // 1
num; // ReferenceError: num is not defined

//전역 변수여서 변경되기 쉽다.
let num = 0;
const increase = function(){
  return ++num;
}
increase();
increase();
num = 3; // 헤엑
increase();

이렇게 변경할 수 있다. 함수를 즉지 실행하여 increase에 클로저를 할당했다.
내부 함수는 외부 즉시 실행 함수가 실행 중일때 정의 되어 외부 실행 함수의 렉시컬 환경을 기억하는 클로저이다.

const increase = (function(){
  let num = 0;
  return function foo(){
    return ++num;
  }
}());
console.log(increase()) // 1
console.log(increase()) // 2

생성자 함수와 함께 사용

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

const counter = new Counter();
console.log(counter.increase())
console.log(counter.increase())
console.log(counter.increase())
console.log(counter.increase())

인크리져, 디크리져 메서드와 같이 사용해봤다. 여기서 두 메서드 모두 같은 변수를 참조하고 있다. 하나의 렉시컬 환경을 공유하기 때문이다.

// 변수 num은 외부에서나, 인스턴스에서 접근할 수 없는 은닉된 변수다.
const Counter = (function(){
  let num = 0;
  function Counter(){};
  Counter.prototype.increase = function(){
    return ++num;
  }
  Counter.prototype.decrease = function(){
    return --num;
  }
  return Counter;
}());
// 프로토타입으로 상속된 increase, decrease 메서드만이 num 값을 참조하고 변경한다.
const counter = new Counter();
console.log(counter.increase())
console.log(counter.increase())
console.log(counter.decrease())
console.log(counter.decrease())
console.log(counter.increase())

캡슐화와 정보 은닉?

캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 한다. 이를 정보 은닉이라고 한다.

자바스크립트의 객체의 모든 프로퍼티와 메서드는 기본적으로 public 하다.

자주 발생하는 실수, for문

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 키워드를 let 키워드로 바꾸면 해결된다.

Why?
var키워드가 함수레벨 스코프인 특성때문이다. for 문의 변수선언문에서 let 키워드로 선언한 초기화 변수를 사용한 for 문은 코드 블록이 반복 실행될 때마다 for문 코드 블록의 새로운 렉시컬 환경을 생성한다. 단 이는 반복문의 코드 블록 내에서 함수를 정의할 때 의미가 있다. 반복문이 생성하는 새로운 렉시컬 환경은 반복 직후 아무도 참조하지 않으면 가비지 컬렉션의 대상이 된다.

var funcs = [];

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


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

0개의 댓글