24장 클로저

클로저는 난해하기로 유명한 자바스크립트의 개념 중 하나다. 클로저는 자바스크립트 고유의 개념이 아니다. 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어(예: 하스켈, 리스프, 얼랭, 스칼라 등)에서 사용되는 중요한 특성이다. 클로저는 자바스크립트 고유의 개념이 아니므로 클로저의 정의가 ECMAScript 사양에 등장하지 않는다. MDN에서는 클로저에 대해 다음과 같이 정의하고 있다.

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

24.1 렉시컬 스코프

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

24.2 함수 객체의 내부 슬롯(Environment)

const x = 1
function foo() {
    const x = 10
    // 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
    // 함수 호출 위치와 상위 스크프는 아무런 관계가 없다.
    bar();
}
// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 [[Environment]]011 저장하여 기억한다.
function bar() {
    console.log(x);
}
foo( ) // ?
bar(); // ?

24.3 클로저와 렉시컬 환경

const x = 1;
// ①
function outer() {
    const x = 10
    const inner = function () { console.log(x); }; // ②
    return inner;
}
// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer(); // ③
innerFunc(); // ④ 10
<html>
    <body>
        <script>
            function foo() {
                const x = 1;
                const y = 2;
                // 일반적으로 클로저라고 하지 않는다.
                function bar() {
                const z = 3;
                debugger;
                // 상위 스코프의 식별자를 참조하지 않는다.
                console.log(z);
                return bar;
            }
            const bar = foo();
            bar();
        </script>
    </body>
</html>
<html>
    <body>
        <script>
            function foo() {
                const x = 1;
                // bar 함수는 클로저였지만 곧바로 소멸한다.
                // 이러한 함수는 일반적으로 클로저라고 하지 않는다.
                function bar() {
                debugger;
                // 상위 스코프의 식별자를 참조한다.
                console.log(x);
                }
                bar();
            }
            foo();
        </script>
    </body>
</html>

24.4 클로저의 활용

// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
    // 카운트 상태를 1만큼 증가시킨다.
    return ++num;
};
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
// 카운트 상태 변경 함수
const increase = function () {
    // 카운트 상태 변수
    let num = 0;
    // 클로저
    return function () {
    // 카운트 상태를 1만큼 증가시킨다.
    return ++num;
    } 
}())
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
const counter = (function () {
    // 카운트 상태 변수
    let num = 0;
    // 클로저인 메서드를 갖는 객체를 반환한다.
    // 객체 리터럴은 스코프를 만들지 않는다.
    // 따라서 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경이다.
    return {
        // num: 0, // 프로퍼티는 public하므로 은닉되지 않는다.
        increase() {
        return ++num;
        },
        decrease() {
        return num > 0 ? --num : 0;
        }
    }
}())
console.log(counter.increase()); // 1
console.log(counter.increaseO);  // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
const Counter = (function () {
    // ① 카운트 상태 변수
    let num = 0;
    function Counter() {
    // this.num = 0; // ② 프로퍼티는 public하므로 은닉되지 않는다.
    }
    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
// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
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
// 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
const counter = (function () {
    // 카운트 상태를 유지하기 위한 자유 변수
    let counter = 0;
    // 함수를 인수로 전달받는 클로저를 반환
    return function (aux) {
    // 인수로 전달받은 보조 함수에 상태 변경을 위임한다.
    counter = aux(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

24.5 캡슐화와 정보 은닉

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
function Person(name, age) {
    this.name = name; // public
    let _age = age; // private
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
    // Person 생성자 함수의 지역 변수 _age를 참조할 수 없다.
    console.log('Hi! My name is ${this.name}. I am ${_age}.');
};
  
const Person = (function () {
    let _age = 0; // private
    // 생성자 함수
    function Person(name, age) {
        this.name = name; // public
        _age = age;
    }
    // 프로토타입 메서드
    Person.prototype.sayHi = function () {
        console.log('Hi! My name is ${this.name}. I am ${_age}.');
    }
    // 생성자 함수를 반환
    return Person;
}())
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
const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30.
console.log(you.name); // Kim
console.log(you._age); // undefined
const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20.
const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30.
// _age 변수 값이 변경된다!
me.sayHi(); // Hi! My name is Lee. I am 30.

24.6 자주 발생하는 실수

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]()); // ②
}
var funcs = [];
for (var i = 0; i < 3; i++){
    funcs[i] = (function (id) { // ① 
        return function () {
            return id;
        } 
    }(i))
}
for (var j = 0; j < funcs.length; j++) {
    console.log(funcs[j]());
}
const funcs = [];
for (let i = 0; i < 3; i++) {
    funcs[i] = function () { return i; };
}
for (let i = 0; i < funcs.length; i++) {
    console.log(funcs[i]()); // 0 1 2
}

끝!