클로저와 함께라면 어디든 갈수있어!

뿌엑·2022년 7월 30일
0

???: 이 시험은,, 웹땔감한테는 적합하지 않습니다,, 이만 돌아가시져

응 아냐~~~

중첩된 함수는 외부 범위(scope)에서 선언한 변수에 접근할 수 있다.
전역변수의 접근을 제한하면서 지역변수로 생성할 때의 메모리 낭비를 막기 위한 방안으로 클로저(Closure) 함수가 사용된다.

클로저 함수는 자바스크립트 파서가 변수를 처리하는 방식이며, 이를 어휘적 범위 지정(lexical scoping)을 고려한다고 한다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<script>
    // 랜덤 정수 생성
    function nextRandomInteger(limit){
        return Math.round(Math.random() * limit)
    }

    // 랜덤 알파벳 리턴(클로저)
    var randomAlphabet = (function(){
        var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        return function(){
            return alphabet.charAt(nextRandomInteger(25));
        }
    })();

    // 양수와 음수로 랜덤 속도 나타냄
    function randomSpeed(maxSpeed){
        return Math.random() * maxSpeed - Math.random() * maxSpeed;
    }

    // MovingText의 생성자 함수
    var canvasWidth = 700;
    var canvasHeight = 400;

    function MovingText(){
        // 위치와 속도 속성
        this.x = nextRandomInteger(canvasWidth);
        this.y = nextRandomInteger(canvasHeight);
        this.vx = randomSpeed(10);
        this.vy = randomSpeed(10);

        // 문서 객체 생성
        this.body = document.createElement('h1');
        this.body.innerHTML = randomAlphabet();
        this.body.style.position = 'absolute';

        // 문서 객체를 document.body에 추가
        document.body.appendChild(this.body);
    }

    MovingText.prototype.move = function(){
        // 범위 검사
        if(this.x < 0 || this.x > canvasWidth){this.vx *= -1;}
        if(this.y < 0 || this.y > canvasHeight){this.vy *= -1;}

        // 이동
        this.x += this.vx;
        this.y += this.vy;

        // 화면에 이동 표시
        this.body.style.left = this.x + 'px';
        this.body.style.top = this.y + 'px';
    }

    window.onload = function(){
            var movingTexts = [];
            
            for(var i = 0; i < 100; i++){
                movingTexts.push(new MovingText());
            }

            setInterval(function(){
                for(var i in movingTexts){
                    movingTexts[i].move();
                }
            }, 1000 / 30);
    };
</script>
<body>
    
</body>
</html>

스코프는 함수를 선언한 위치에 따라 결정된다. 이를 렉시컬 스코핑(Lexical scoping)이라 한다.

    // 랜덤 알파벳 리턴(클로저)
    var randomAlphabet = (function(){
        var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        return function(){
            return alphabet.charAt(nextRandomInteger(25));
        }
    })();

위 예제에서 랜덤 알파벳을 반환하는 익명함수는 randomAlphabet 변수에 저장되는 익명함수를 상위 스코프로 둔다. 만일 함수가 전역에 선언되었다면 해당 함수의 상위 스코프는 전역 스코프가 된다.

내부함수가 호출되면 해당 함수의 실행 컨텍스트가 실행 컨텍스트 스택에 쌓이고 변수 객체(Variable Object)와 스코프 체인(Scope chain), 그리고 this에 바인딩할 객체가 결정된다.
이때 스코프 체인은 전역 스코프를 가리키는 전역 객체와 상위 스코프를 가리키는 상위 함수의 활성 객체(Activation object), 그리고 함수 자신의 스코프를 가리키는 활성 객체를 순차적으로 바인딩한다. 스코프 체인이 바인딩한 객체가 렉시컬 스코프의 실체이다.

내부함수가 외부함수의 변수에 접근할 수 있는 건 렉시컬 스코프의 레퍼런스를 저장하고 있는 실행 컨텍스트의 스코프 체인을 자바스크립트 엔진이 검색하여 가능하다.

randomAlphabet 변수는 외부 익명함수를 실행 컨텍스트에 등록한다. randomAlphabet과 같이 내부 익명함수를 실행 컨텍스트에 등록하면 내부 익명함수는 외부 익명함수의 변수 alphabet을 사용해 무작위한 알파벳을 반환한다.

이처럼 자신을 포함하는 외부함수보다 내부함수가 더 오래 유지될 때, 외부함수 밖에서 내부함수가 호출돼도 외부함수의 지역 변수에 접근할 수 있다.

이를 클로저라 한다.

간단한 예시를 하나 더 들어보면 다음과 같다.

    <script>
        function outerFunc() {
            var x = 5;

            var innerFunc = function () {
                console.log(x);
            };
            return innerFunc;
        }

        outerFunc()();
    </script>

console에 찍히는 실행값은 5이다.

MDN에선 클로저를 이와 같이 정의한다.

A closure is the combination of a function and the lexical environment within which that function was declared.
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.

위 정의에서의 함수는 반환된 내부함수이며, 함수가 선언될 때의 렉시컬 환경은 내부함수가 선언됐을 때의 스코프를 의미한다. 클로저는 반환된 내부함수가 자신이 선언됐을 때의 렉시컬 스코프를 기억하며, 렉시컬 스포크 밖에서 호출돼도 해당 스코프에 접근할 수 있다.
클로저에 의해 참조되는 외부함수의 변수를 자유변수(Free variable)이라 부른다. 클로저는 자유변수에 함수가 닫혀있단 의미로 다시말해 자유변수에 엮인 함수란 것이다.

외부함수가 이미 반환됐어도 외부함수 내 변수는 그를 참조하는 내부함수가 있는 한 유지된다. 내부함수는 외부함수의 복사본이 아닌, 실제 위치에 접근한다.

<script>
    const increase = (function () {
        let num = 0;

        return function () {
            return ++num;
        };
    }());

    console.log(increase());
    console.log(increase());
    console.log(increase());
</script>

increase()를 실행하면 increase 함수에 저장된 num이 하나씩 증가한다.

html의 요소에 바인딩한 상태로 클로저를 사용할 수도 있다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<script>
    window.onload = function(){
        const btn = document.querySelector('button');
        
        btn.addEventListener('click', handleClick());

        function handleClick(){
            let count = 0;
            return function(){
                count++;
                console.log('count', count);

                return count;
            }
        }
    }
</script>
<body>
    <button id="button">클로저 테스트</button>
</body>
</html>

-- 한동안의 폭풍 클릭 후....

handleClick 함수 내 선언된 변수 count가 소멸되지 않고 계속 참조되는 것을 확인할 수 있다.

0개의 댓글