24장 : 클로저
MDN에서 정의된 클로저란?
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
const x = 1;
function outerFunc() {
const x = 10;
innerFunc();
}
function innerFunc() {
console.log(x); // 1
}
outerFunc();
- innerFunc 함수를 outerFunc 함수의 내부에서 호출한다 하더라도 outerFunc 함수의 변수에 접근할 수 없다
- 자바스크립트는 렉시컬 스코프를 따르는 프로그래밍 언어이기 때문이다
1. 렉시컬 스코프(= 정적 스코프)
- 자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디서 정의했는지에 따라 상위 스코프를 결정한다.
- 함수를 어디서 호출하는지는 상위 스코프 결정에 어떠한 영향도 주지 못한다.
- 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다. 이것이 바로 렉시컬 스코프이다.
2. 함수 객체의 내부 슬롯 [[Environment]]
- 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.
const x = 1;
function foo() {
const x = 10;
bar();
}
// bar 는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 [[Environment]]에 저장하여 기억한다
function bar() {
console.log(x);
}
foo();
bar();
3. 클로저와 렉시컬 환경
- 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저(clousure)라고 부른다.
const x = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
// outer 호출 시 중첩 함수 inner 를 return
// 그리고 outer 의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거된다.
const innerFunc = outer();
innerFunc(); // 10
- 중첩 함수 inner 를 반환하고 생명 주기를 마감한 outer 함수의 변수 x 의 값을 어떻게 참조할 수 있을까?
- 외부 함수보다 더 오래 생존한 중첩 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프를 기억하기 때문에, 상위 스코프를 참조할 수 있기 때문이다!
- 따라서 inner 함수는 상위 스코프 outer 함수의 변수 x 의 값을 참조할 수 있다.
4. 클로저의 활용
- 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.
- 즉, 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.
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
- increaser 와 decreaser 는 각각 makeCounter 함수 호출 시 생성된 자신만의 독립된 렉시컬 환경을 갖는다.
5. 캡슐화와 정보 은닉
- 캡슐화(encapsulation) : 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것
- 캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉이라 한다.
- 자바스크립트는 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
- name 프로퍼티는 외부로 공개되어 있어 자유롭게 참조하거나 변경할 수 있어 public 하다.
- _age 변수는 Person 생성자 함수의 지역 변수이므로 외부 함수에서 참조하거나 변경할 수 없어 private 하다.
6. ⭐클로저 사용 시 let과 var⭐
1. var : 함수 스코프
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
}
- var 로 선언된 i 변수는 함수 스코프를 가지기 때문에 모든 루프에서 같은 스코프를 공유한다.
- for 문을 돌면서 funcs 배열에는 각 익명 함수가 할당되고, 전역 변수 i는 for 문을 돌면서 3 이 된다.
- ex )
- func[0] = function ()
- func[1] = function ()
- ...
- for 문 종료 후 각 함수들이 실행되는데, 여기서 이미 전역 변수 i는 3이므로 각 배열의 값은 모두 3이 출력된다.
2. let : 블록 스코프
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
return i;
};
}
for (let j = 0; j < funcs.length; j++) {
console.log(funcs[j]()); // 0 1 2
}
- let 으로 선언된 i 변수는 블록 스코프를 가지기 때문에 각 루프에서 새로운 스코프를 생성하고 그 스코프는 해당 루프에서의 i 변수를 기억한다.
- 배열에 저장된 각 함수는 함수가 생성된 시점에서의 독립적인 스코프를 가지고 있다.
- 따라서 for 문 종료 후 각 함수들이 실행될 때의 i 값을 참조하기 때문에 각 배열들의 값은 0, 1, 2 가 된다!
Deep Dive Study week - 01
'[Study] Deep Dive 스터디' 카테고리의 다른 글
[JS] 이터러블 (0) | 2024.01.23 |
---|---|
[JS] 프로미스 (0) | 2024.01.22 |
[JS] 비동기 프로그래밍 (0) | 2024.01.20 |
[JS] ES6 함수의 추가 기능 (0) | 2024.01.16 |
[JS] this (1) | 2024.01.13 |