본문으로 바로가기

closure

category JavaScript/기초 2020. 10. 3. 00:43

closure

내가 배우고 이해한 걸로 스코프를 설명하고 싶지만 도저히 요약이 되지 않아서 '인사이드 자바스크립트'에서는 풀어서 쓴 정의를 인용하겠다

'클로저란 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수'라고 얘기하고있다

생명 주기가 끝난 외부 함수의 변수를 참조하려면 어떻게 해야할까?

사실 말 속에 이미 답이 있다

외부 함수의 스코프를 참조 할 수 있는 내부 함수의 리턴 값을 받아 사용하면 된다

    function a(){
         var out = 100;
        return function b(){
            console.log(out);
        }
    }

    var test = a();
    test();

function b()를 리턴 받은 test()는

console.log(out)을 실행하게 된다

하지만 b의 스코프안에는 out이 없어 상위 함수인 a의 스코프에서 out을 찾아서 출력하게 되는데

이처럼 생명 주기가 끝났어도 스코프를 참조할 수 있는 성질 혹은 함수 자체를 클로저라고 부른다

클로저의 활용

Private Member

  1. 외부로부터의 접근제한
  2. 전역스코프의 변수 최소화

등의 이유 때문에 private를 사용하는데

자바스크립트에서는 private member를 클로저로 흉내낼 수 있다

간단한 코드로 살펴보자

    var account = {
        cash : 10000,
        deposit : function (value) {
            return this.cash = this.cash + value;
        },
        withdraw : function (value) {
            if(this.cash >=value) return this.cash = this.cash - value;
            else return console.log('잔액 부족');
        }
    }

와 같은 입,출금 서비스를 하는 아주 심플한 코드를 만들었다

문제점이 보이는가?

account.cash = 1000000;

같은 코드를 이용해서 너무나 손쉽게 잔액을 불리고 줄일 수 있게 되어있다

이런 문제를 방지하기 위해서 클로저를 이용한 private 멤버를 만들 수 있는데

적용해 보면

    var account = function (c) {
        var cash = c;
        return {
            deposit : function (money) {
                return cash += money;
            },
            withdraw : function (money) {
                if(cash >= money) return cash -= money;
                else return console.log('잔액 부족');
            }
        }
    }
    myMoney = account(10000);

이제 이 코드는

myMoney.deposit();
myMoney.withdraw();

두 메소드를 이용해서 입금과 출금은 가능하지만 잔액에는 직접적인 접근이 불가능하게 됐다

루프 안에서의 클로저

이건 활용법이라기 보다 주의해야할 상황이다

너무나도 많이 돌아다니는 예시 두개를 들고 와 봤다

  1. 첫번째 예시
    var arr = [];
    for(var i=0; i<5;i++) {
        arr[i] = function () {
            return i;
        }
    }
    for(var index in arr){
        console.log(arr[index]());
    }

위 코드의 결과 (5앞의 5는 그만큼 반복되었다는 얘기다)

자바스크립트 입문자라면 대부분 그랬겠지만 나는 아래 반복문의 출력값이 당연히 0,1,2,3,4 일 줄 알았다

위 반복문에서 arr배열의 인덱스에 할당되는 것은 return 값이 아니라 익명함수 그 자체다

따라서 반복문이 끝나면 인덱스에는 다음과 같이 할당되어 있을 것이다
arr[0] = function () { return i; }
arr[1] = function () { return i; }
arr[2] = function () { return i; }
arr[3] = function () { return i; }

이상태에서 아래 반복문 처럼 각 배열에 들어가있는 함수들을 실행한다면?

i를 리턴받아야하는데 i가 없다.. 어디서 받아와야할까?

그렇다 i의 상위 스코프인 for문에 변수 i가 정의되어있기 때문에 거기서 참조해야한다

그렇다면 i의 값은 어떻게 될까?

이미 조건을 만족하고 끝난 반복문의 i값은 5다

그렇기 때문에 arr[0]~arr[4]까지 상위 스코프에서 참조 받아 리턴하는 i값은 모두 동일하게 5가 출력되는 것이다

뭘 고쳐주면 될까?

    var arr = [];
    for(var i=0; i<5;i++) {
        arr[i] = function (id) {
            return function () {
                return id;
            }
        }(i);
    }
    for(var index in arr){
        console.log(arr[index]());
    }

배열에 넣을 함수를 한번 더 감싼 뒤에 반복문을 도는 당시의 i값을 인자로 받아서 스코프에 저장해두는 것이다

그렇게 하면 밑의 반복문에서 i값을 참조할 때 for의 i가 아닌 바로 상위의 id를 인자값으로 받은 스코프의 i값을 받아와 우리가 원하는 결과가 나온다

위 코드의 결과값

  1. 두번째 예시

아래의 코드는 1 2 3 4 5의 코드를 1초마다 콘솔창에 찍으려는 의도를 갖고 만든 코드다

    function countTime(time) {
        for(var i=1; i<=time; i++) {
            setTimeout(function () {
                console.log(i);
            },i * 1000);
         }
     }
    countTime(5);

나는 이 코드를 '인사이드 자바스크립트' 책에서 보고 이해하는게 정말 힘들었다..

우선 이를 이해하기 위해선 호출 스택과 이벤트 루프에 대해 이해하고 있어야하는데

나는 그 부분에 무지해서 따로 공부를 하고 다시 이 코드를 공부했다

간단하게 설명하면 setTimeout()같은 비동기 함수는 반복문이 다 돌고 나서 나중에 실행되는 함수다

그렇기 때문에 반복문이 돈 상태에서
console.log(i);
를 어내려면 1번에서 봤던 코드와 같이 조건을 만족한 for문의 i를 참조하게되고 6이 5개 나오게 된다

위 코드의 결과값

    function countTime(time) {
        for(var i=1; i<=time; i++) {
            (function (ci) {
                setTimeout(function () {
                    console.log(ci);
                },i * 1000);
            })(i);
        }
    }

이를 해결하려면 1번과 같이 즉시 실행 함수를 사용한다

setTimeOut을 익명함수로 감싸고 익명함수의 인자로 for문을 하나하나의 인자를 받아 스코프에 저장해

각각 5개의 스코프를 따로 참조하게 되어 우리가 원하는 결과값을 찍어낸다

위 코드의 결과값

 

'JavaScript > 기초' 카테고리의 다른 글

Array.sort  (0) 2020.10.22
prototype  (0) 2020.10.03
호출 스택, 이벤트 루프  (0) 2020.10.02
생성자 함수  (0) 2020.10.01
this binding  (0) 2020.09.30