클로저는 내부 함수로부터
외부함수
에의 접근 권한을 준다
클로저
는 함수 생성 시점에 언제나 생긴다
const x = 1;
function outerFunc() {
const x = 10;
function innerFunc() {
console.log(x); // 10
} //innerFunc와 outerFunc 사이의 closure
innerFunc();
} // outerFunc와 전역컨텍스트 사이의 closure
outerFunc();
자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라, 어디에 정의했는지에 따라 상위 스코프를 결정한다.
렉시컬 스코프
라 한다.
const x = 1;
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // ? -> 1
bar(); // ? -> 1
foo와 bar 함수의 상위 스코프는 전역
이다. 그러므로, 상위 스코프에 대한 참조값이 전역 컨텍스트에서 결정되어서 foo(), bar()의 값이 모두 1로 평가된다.
함수는 자신의 내부 슬롯
[[Environment]]
에 자신이 정의된 환경,
즉, 상위 스코프의 참조를 저장한다.
const x = 1;
// ①
function outer() {
const x = 10;
const inner = function () { console.log(x); }; // ②
return inner;
}
// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer(); // ③
innerFunc(); // ④ 10
outer함수는 중첩 함수 inner를 반환하면서 생명주기가 종료된다.
즉, outer함수의 실행이 종료되면 outer함수의 실행 컨텍스트가 실행 컨텍스트 스택에서 제거
되어 더이상 outer함수의 지역변수 x
는 유효하지 않다
그런데 왜 10이 나오느냐?
outer함수의 실행 컨텍스트는 실행 컨텍스트 스택
에서 제거
되지만, outer함수의 렉시컬 환경
까지 소멸하는 것은 아니다
innerFunc가 받는 것은 inner [[Environment]] outer L/E
이므로
inner의 상위스코프인 outer의 x를 참조하여 반환한다.
이처럼 외부 함수보다 중첩함수
가 더 오래 유지되면,
중첩 함수는 이미 생명 주기
가 종료된 외부 함수의 변수를 참조 할 수 있으며
이러한 중첩 함수
를 클로저
라고 한다
<!DOCTYPE html>
<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>
<!DOCTYPE html>
<html>
<body>
<script>
function foo() {
const x = 1;
// 일반적으로 클로저라고 하지 않는다.
// bar 함수는 클로저였지만 곧바로 소멸한다.
function bar() {
debugger;
// 상위 스코프의 식별자를 참조한다.
console.log(x);
}
bar();
}
foo();
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<script>
function foo() {
const x = 1;
const y = 2;
// 클로저
// 중첩 함수 bar는 외부 함수보다 더 오래 유지되며 상위 스코프의 식별자를 참조한다.
function bar() {
debugger;
console.log(x);
}
return bar;
}
const bar = foo();
bar();
</script>
</body>
</html>
위의 예제의 경우 클로저
에 의해 참조
되는 상위 스코프의 변수를 자유 변수
라고 한다.
즉, 클로저란 함수가 자유변수에 대해 닫혀있다라는 의미이다.
또는 자유 변수에 묶여있는 함수이다
또한, 클로저는 상위 스코프를 기억해야 하기에 불필요한 메모리 점유를 걱정할 수도 있겠지만, 사실 클로저의 메모리 점유는 필요한
것을 기억하기 위함이므로 딱히 걱정할 대상은 아니기에 필요하면 적극적으로 활용하는 것이 좋다
그렇다면 어떻게 클로저를 활용할까,,,?
클로저는
상태
를 안전하게변경
하고유지
하기 위해 사용된다.
상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용한다
// 카운트 상태 변경 함수
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.increase()); // 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
대부분의 객체 지향 프로그래밍 언어는 클래스를 정의하고, 그 클래스를 구성하는 멤버에 대하여 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
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 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.
me.sayHi(); // Hi! My name is Lee. I am 30.
//하나의 생성자만 형성하기에 age가 바뀜
console.log(you.name); // Kim
console.log(you._age); // undefined
바뀐 문법으로 적용한 클로저 함수
class Person {
#age;
constructor(name, age){
this.name = name;
this.#age = age;
}
sayHi(){
console.log(`Hi! My name is ${this.name}. I am ${this.#age}.`);
}
}
const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20.
console.log(me.name); // Lee
me.#age // SyntaxError -> Private
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
}
전역 컨텍스트 i를 참조하고 있기에!!
이를 블록 스코프
변수로 바꿔주면 해결이 된다
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
}