클로저는 자바스크립트 고유의 개념이 아닌 함수를 일급 객채로 취급하는 함수형 프로그래밍 언어에서 사용되는 특성이다.
"클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다" - MDN
자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 정의한다.
또한 렉시컬 환경의 외부 렉시컬 환경에 대한 참조
에 저장할 참조값. 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경에 의해 결정된다.
이를 렉시컬 스코프라고 한다.
함수는 자신의 내부 슬롯 [[Environment]]
에 자신이 정의된 환경인 상위 스코프의 참조를 저장한다.
함수내부에서 정의된 함수 표현식
은 외부 함수 코드가 실행되는 시점
에 평가되어 함수 객체를 생성한다.
(블록 단위로 실행 컨텍스트가 생성되기 때문에 외부 함수가 실행 컨텍스트 스택위에서 실행 되고 있을 때 내부 함수 표현식이 평가된다.)
따라서 함수 객체의 내부 슬롯 [[Environment]]
에 저장될 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프다.
또한 자신이 호출되었을 때 생설될 함수 렉시컬 환경의 외부 렉시컬 환경에 대한 참조
에 저장될 참조값이다.
함수 객체는 내부 슬롯 [[Environment]]
에 저장한 렉시컬 환경의 참조, 즉 상위 스코프를 자신이 존재하는 한 기억한다.
정리해보면
[[Environment]]
에는 상위 렉시컬 환경(현재 실행중인 실행 컨텍스트의 렉시컬 환경)이 저장된다.외부 렉시컬 환경에 대한 참조
에는 위에서 말한 [[Environment]]
에 저장된 참조 값으로 연결된다.const x = 1;
function outer() {
const x = 10;
const inner = function () {console.log(x)};
return inner;
}
const innerFunc = outer();
innerFunc();
위 코드의 결과는 10이 나온다.
그냥 기본적으로 생각해보면 innerFunc
에는 outer
내부의 inner
함수가 return 되어 들어가게 된다.
따라서 innerFunc
를 실행했을때 x는 1일거 같다.
하지만 여기서 이걸 다시 상기시켜야 한다.
자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 정의한다.
이 말에 따른다면 inner
의 상위스코프는 outer
이다. 그렇기 때문에 여기서 x는 outer
의 x를 항상 가리키고 있는다.
따라서 결과는 10이 나오게 되는 것이다.
이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다.
위 코드의 동작 과정은
[[Environment]]
에 전역 렉시컬 환경을 저장한다.외부 렉시컬 환경에 대한 참조에
outer 함수의 [[Environment]]
에 저장된 렉시컬 환경인 전역 렉시컬 환경을 할당한다.[[Environment]]
에 현재 실행중인 실행컨텍스트인 outer 함수의 렉시컬 환경을 저장한다.외부 렉시컬 환경에 대한 참조
에는 inner 함수의 [[Environment]]
에 저장된 렉시컬 환경인 outer 렉시컬 환경이 저장된다.console.log(x)
의 x는 가장 가까운 outer 렉시컬 환경의 10을 가지게 되고 10이 출력되게 되는 것이다.
📌 실행컨텍스트 스택의 순서는 전역 -> outer -> outer pop -> inner 이 순서이다
❓ 이렇게 보면 모든 함수가 다 클로저인가라고 생각할 수 있지만 일반적으로는 모두를 클로저라고 부르진 않는다.
<!DOCTYPE html>
<html lang="en">
<body>
<script>
function foo() {
const x = 1;
function bar() {
const z = 3;
debugger;
console.log(z);
}
return bar;
}
const bar = foo();
bar();
</script>
</body>
</html>
위 코드를 실행시켜보면 개발자도구에서 아래와 같은 결과가 나온다.
그 이유는 foo가 bar를 반환하지만 bar는 상위 스코프의 값을 참조하지 않기 떄문이다.
그럼 closer가 나오게 코드를 구현해보면
<!DOCTYPE html>
<html lang="en">
<body>
<script>
function foo() {
const x = 1;
function bar() {
debugger;
console.log(x);
}
bar();
}
foo();
</script>
</body>
</html
이 코드는 bar가 상위스코프인 foo의 값을 참조하고 있기 때문에 closer라고 나오지만 일반적으로 클로저라고 하진 않는다.
📌 bar는 상위스코프인 foo보다 생명주기가 짧다 (bar를 반환하지 않기 때문에)
❗️ 그렇다면 이 아래 코드의 bar를 진짜 클로저라고 할 수 있다
<!DOCTYPE html>
<html lang="en">
<body>
<script>
function foo() {
const x = 1;
function bar() {
debugger;
console.log(x);
}
return bar;
}
const bar = foo();
bar();
</script>
</body>
</html>
📌 이처럼 외부 함수보다 중첨 함수가 더 오래 유지되는 경우 중첩함수는 이미 생명주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다.
📌 클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에만 한정하는 것이 일반적이다.
📌 이 참조되는 변수를 자유변수라고 부르고 클로저란 "함수가 자유변수에 대해 닫혀있다." 즉 "자유변수에 묶여있는 함수"라고 할 수 있다.
❓ 전역변수를 지양해야 하는 것처럼 그럼 클로저는 상위 스코프를 기억해야 하기 떄문에 상위 스코프의 렉시컬 환경이 계속 남아있는 것이기 때문에 메모리적으로 안좋지 않을까.
❗️ 자바스크립트 엔진은 클로저가 참조하고 있지 않는 식별자는 기억하지 않기 때문에 메모리 걱정은 안해도 된다고 한다.
이러한 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.
즉 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용되는 것이 클로저이다.
안좋은 코드들을 먼저 보면
// 안좋은 예시 1
let num = 0;
const increase = function (){
return ++num;
}
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
// 안좋은 예시 2
const increase = function(){
let num = 0;
return ++num;
}
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
📌 따라서 이 num 값을 increase만 접근할 수 있도록 숨겨야 한다.
❗️ 이 두가지 경우를 모두 해결하기 위해 클로저를 사용할 수 있다.
const counter = (function (){
let num = 0;
return function(){
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
이렇게 하면 counter 내부에 num을 숨겨 상태를 보존할 수 있고 즉시 실행함수이기 때문에 여러번 실행 되지 않아 상태를 유지할 수 있다.
생성자 함수로 나타내면 아래와 같다
const Counter = (function() {
let num = 0;
function Counter(){
// this.num = 0; 이렇게 하면 은닉되지 않는다.
}
Counter.prototype.increase = function(){
return ++num;
}
Counter.prototype.decrease = function(){
return num > 0 ? --num : 0;
}console.log(counter.decrease()); // 3
return Counter
}());
함수형 프로그래밍은 외부 상태 변경이나 가변 데이터를 피하고 불변성을 지향하기 때문에 클로저를 적극적으로 사용한다.
function makeCounter(aux){
let counter = 0;
return function() {
counter = aux(counter);
return counter;
};
}
function increase(n){
return ++n;
}
function decrease(n){
return --n;
}
const increaser = makeCounter(increase)
console.log(increaser()); // 1
console.log(increaser()); // 2
const decreaser = makeCounter(decrease)
console.log(decreaser()); // -1
console.log(decreaser()); // -2
위의 코드는 클로저를 사용해서 counter 변수를 잘 숨겼다 하지만 makeCounter 함수를 두번 호출했기 때문에 렉시컬 환경이 두개가 생겨서 counter를 공유하지 않는다.
이건 아래 그림을 보면 이해가 잘 될것이다.
간단히 설명하면
[[Environment]]
에 이 렉시컬 환경이 저장된다.[[Environment]]
에 이 렉시컬 환경이 저장된다.이러한 문제를 해결하기 위해서는 아래처럼 즉시 실행 함수를 사용해 한번만 호출하도록 한다.
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
캡슐화는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는것을 말한다. 캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보은닉이라고 한다.
자바와 같은 객체지향 프로그래밍 언어는 클래스를 정의할때 public, private, protected같은 접근 제한자를 선언하여 공개 범위를 지정할 수 있지만 자바스크립트는 없기 때문에 모든 프로퍼티와 메서드가 public 이다.
❗️최근에는 private할수 있는 방법이 나왔다고 한다.
이를 비슷하게 구현하기 위해선 아래와 같이 할 수 있다.
function Person(name, age){
this.name = name;
let _age = age;
this.sayHi = function(){
console.log(`${this.name} ${_age}`);
}
}
const me = new Person('Lee', 20);
const you = new Person('Kim', 30);
me.sayHi() // Lee 20
you.sayHi() // Kim 30
console.log(me._age) // undefined
이렇게 하면 _age 변수를 private하게 만들 수 있다.
📌 이 과정에서 sayHi 메서드가 두번 생성되기 때문에 중복을 방지하기 위해 prototype을 사용하면 프로토타입은 또 한번만 생성되기 때문에 _age
변수의 값을 하나로만 밖에 못가지기 때문에 방법이 없다.
-> 따라서 최근에 나온 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]());
}
이렇게 코드를 짜는 사람은 없겠지만 클로저를 활용해서 해결하는 것과 var가 왜 없어졌는지를 설명할 수 있는 좋은 예시인것 같다.
그냥 보면 0, 1, 2 잘 나올것 같지만 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]());
}
해결된 방법은
근데 이건 그냥 var가 문제인 거라서 var를 let으로 바꾸면 해결된다.
그 이유는 실행컨텍스트에서 배운것 처럼 for문은 실행될때 마다 렉시컬 환경을 계속 만들기 때문에 funcs
에 들어있는 함수가 그 렉시컬 환경을 참조하기 때문에 0, 1, 2를 참조하게 되는 것이다.
그후에 아무도 참조하지 않으면 가비지컬렉터가 가져가기 때문에 메모리 걱정은 할필요가 없다.
📌 함수형 프로그래밍에서 클로저를 활용을 많이 한다고 하는데 사실 리액트 하면서 사용해 본적이 없다. 더 다양한 예시를 찾아보면서 클로저를 한번 사용해 봐야겠다.
제로초님의 유튜브에서 나온 예시인데 좋은 예시 같아서 추가한다.
function a() {
for(var i = 0; i < 3; i++){
setTimeout(()=>{
console.log(i);
}, i * 1000)
}
}
a();
위 코드의 결과는 0, 1, 2가 되었으면 좋겠지만 3, 3, 3이 나온다.
그 이유는
이것도 그냥 let으로 바꾸면 해결되지만 클로저 배운김에 해결해 보면
function a() {
for(var i = 0; i < 3; i++){
(function(id){
setTimeout(()=>{
console.log(id);
}, i * 1000)
}(i))
}
}
a();
이렇게 하면 setTimeout의 콜백함수는 즉시실행 함수를 상위 스코프로 기억하고 상위 스코프인 function은 0, 1, 2라는 값을 가지고 있기 때문에 0, 1, 2가 출력된다.
참고 :
https://www.inflearn.com/course/%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%94%A5%EB%8B%A4%EC%9D%B4%EB%B8%8C
https://www.youtube.com/watch?v=4hhlfq3Uy6U