클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
함수가 선언된 렉시컬 환경
const x = 1;
function outerFunc() {
const x = 10;
function innerFunc() {
console.log(x); // 10
}
innerFunc();
}
outerFunc();
console.log(x)
에서 참조하고 있는 x값은
scope chain
에 의해 바로 바깥쪽 scope
를 찾는다.
LexicalEnvironment
를 갖고 있다.console.log(x)
는 10이 출력된다.JS엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다.
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // 1
bar(); // 1
다시 말하면, 외부 렉시컬 환경에 대한 참조에 저장할 참조값, 즉, 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)
에 의해 결정된다.
b. 정의된 환경에 대한 정보를 저장하는 곳 : outer
위에서 본 예제처럼 함수가 정의된 환경(위치)과 호출된 환경(위치)은 다를 수 있다. 따라서 호출되는 환경과는 상관없이 정의된 환경에 대한 정보를 LexicalEnvironment > outer
가 기억한다.
const x = 1;
function foo() {
const x = 10;
// 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
// 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
bar();
}
// 여기보세요 여기!
// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 저장하여
// 기억한다.
function bar() {
console.log(x);
}
foo();
bar();
c. 클로저와 렉시컬 환경 (Lexical Environemnt)
외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 여전히
참조할 수 있다 ← 여기서 중첩 함수가 바로 클로저이다.
const x = 1;
// 1
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
const innerFunc = outer();
innerFunc();
outer
함수를 호출하면 중첩 함수 inner
를 반환한다.
그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스탭에서 팝되어 제거된다 (역할을 다 했으니)
inner 함수는 런타임에 평가된다.
inner함수가 innerFunc에 전달되었는데, 이는 outer 함수의 렉시컬환경을 (여전히)참조하고 있다.
즉, outer함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer함수의 Lexical Environment까지 소멸하는 것이 아니다.
→ 이게 가능한 이유는 가비지 컬렉터는 안쓰는 것만 가져가기 때문이다. outer함수의 렉시컬 환경은 참조하는 곳이 있으니 놔두는 것!
상위 스코프의 식별자를 참조하지 않으면 클로저가 아니다.
function foo() {
const x = 1;
const y = 2;
// 일반적으로 클로저라고 하지 않아요.
function bar() {
const z = 3;
//상위 스코프의 식별자를 참조하지 않기 때문이죠.
console.log(z);
}
return bar;
}
const bar = foo();
bar();
외부함수보다 먼저 소멸하는 것은 클로저가 아니다.
function foo() {
const x = 1;
// bar 함수는 클로저였지만 곧바로 소멸한다.
// 외부로 나가서 따로 호출되는게 아니라, 선언 후 바로
// 실행 + 소멸
// 이러한 함수는 일반적으로 클로저라고 하지 않는다.
function bar() {
debugger;
//상위 스코프의 식별자를 참조한다.
console.log(x);
}
bar();
}
foo();
외부함수보다 더 오래 유지되며, 상위 스코프의 식별자를 참조할 때 클로저이다.
function foo() {
const x = 1;
const y = 2;
// 클로저의 예
// 중첩 함수 bar는 외부 함수보다 더 오래 유지되며
// 상위 스코프의 식별자를 참조한다.
function bar() {
debugger;
console.log(x);
}
return bar;
}
const bar = foo();
bar();
클로저는 JS의 강력한 기능으로, 필요하면 적극 활용해야한다. 클로저의 유용한 예시를 알아보자.
클로저는 주로 상태를 안전하게 변경하고 유지하기 위해 사용한다. 의도치 않은 상태의 변경을 막기 위해서.
상태를 안전하게 은닉한다(특정 함수에게만 상태 변경을 허용한다)는 표현을 기억하자.
예제 (카운터)
// 카운트 상태 변경 함수 #1
// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터를 구현해요!
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
console.log(increase());
// num = 100; // 치명적인 단점이 있어요.
console.log(increase());
console.log(increase());
// 카운트 상태 변경 함수 #2
const increase = function () {
// 카운트 상태 변수
let num = 0;
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
// 이전 상태값을 유지 못함
console.log(increase()); //1
console.log(increase()); //1
console.log(increase()); //1
이럴때 의도치 않은 변경은 방지하면서, 이전 상태를 유지해야할 때 클로저를 사용한다.
// 카운트 상태 변경 함수 #3
const increase = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
return ++num;
};
})();
// 이전 상태값을 유지
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3
<aside> 💡 정리
Closure(클로저)는 State(상태)가 의도치 않게 변경되지 않도록 안전하게 은닉(information hiding)하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용한다.
</aside>
조금 더 확장 해보기
// 카운트 상태 변경 함수 #4
// 클로저 카운트 기능 확장(값 감소 기능 추가)
const counter = (function () {
//카운트 상태 변수
let num = 0;
// 클로저인 메서드(increase, decrease)를 갖는 객체를 반환한다.
// property는 public -> 은닉되지 않는다.
return {
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
캡슐화란 무엇일까?
프로퍼티와 메서드를 하나로 묶는 것을 말한다.
객체의 특정 프로퍼티
나 메서드
를 감출 목적으로 사용한다. 가지고 있는 정보가 예민하고 민감해서 정보 은닉이 필요한 경우이다.
java 등 기타 객체지향 언어에서 사용하는 public, private, protected
→ 접근을 제한할 수 있는 기능이다.
자바스크립트는 제공하지 않는다.
→ 즉, 별도의 조치를 취하지 않으면 기본적으로 외부 공개가 된다는 의미이다.
// 생성자 함수
function Person(name, age) {
this.name = name; //public
let _age = age; //private
// 인스턴스 메서드
// 따라서, Person 객체가 생성될 때 마다 중복 생성됨
// : 해결방법 -> prototype
this.sayHi = function () {
console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};
}
const me = new Person("Choi", 33);
me.sayHi(); // Hi!, My name is Choi. I am 33.
console.log(me.name); // Choi
console.log(me._age); // undefined
const you = new Person("Lee", 30);
you.sayHi(); // Hi! My name is Lee. I am 30.
console.log(you.name); // Lee
console.log(you.age); // undefined
다음 코드를 보자.
아래 코드에서 var
로 선언한 i
는 함수레밸 스코프이기 때문에 for 루프
의 블록레밸 스코프를 무시한다.
→ funcs[i]
에 전달된 함수를 실행할 때 i는 for루프가 종료된 시점의 i
를 전달 받는다.
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]());
}
// for 문의 변수 선언문에서 var 키워드로 선언한 i 변수는 "블록 레벨 스코프"가 아닌
// "함수레벨 스코프"이다.
// expectation
// 0, 1, 2
// result
// 3, 3, 3
즉시실행 함수로 바꾸기.
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]());
}
let (블록레밸 스코프) 사용하기
var 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
}