MDN에서는 클로저에 대해 다음과 같이 정의한다.
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
렉시컬 스코프(Lexical Scope)는 함수를 어디에 정의했는지에 따라 해당 함수의 상위 스코프를 결정하는 개념이다. 다시 말해, 정적 스코프(static scope)라고도 불린다.
렉시컬 스코프는 함수가 선언된 위치에 기반하여 변수나 함수를 참조할 때 어떤 스코프를 탐색해야 하는지 결정한다. 함수가 정의될 때 함수의 상위 스코프에 대한 참조가 정적으로 결정되며, 이 참조는 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"라는 내부 슬롯에 저장된다.
예를 들면,
function outerFunc() {
const x = 10;
function innerFunc() {
console.log(x); // 10
}
innerFunc();
}
outerFunc();
inner 함수는 outer 함수 내에 정의되어 있어 outer 함수의 변수에 접근할 수 있다. (렉시컬 스코프에 따라 결정) inner 함수가 호출될 때, 렉시컬 스코프는 함수가 정의된 시점에 결정된 상위 스코프를 사용하여 변수 x를 찾는다. inner 함수는 outer 함수 내에 정의되었으므로 outer 함수의 스코프를 상위 스코프로 갖게 된다. 따라서 inner 함수는 x를 참조하여 출력할 수 있다.
const x = 1;
function outer() {
const x = 10;
const inner = function () { console.log(x); }
return inner;
}
const innerFunc = outer();
innerFunc(); // 10;
스코프 체인은 상위 스코프에 대한 참조들의 연결로 구성되며, 클로저는 이러한 스코프 체인을 통해 외부 변수에 접근할 수 있다.
inner 함수는 outer 함수 내부에서 정의되었으며, outer 함수의 실행 도중에 생성되었다. inner 함수는 outer 함수가 반환된 이후에도 innerFunc 변수에 할당되어 계속해서 참조할 수 있다. 이렇게 inner 함수가 outer 함수보다 더 오래 유지되는 경우, inner 함수는 이미 생명주기가 종료한 outer 함수의 변수를 참조할 수 있는데 이를 클로저 라고 부른다.
💡 왜 전역변수 1을 참조하지 않는걸까?
클로저는 함수가 정의된 시점의 렉시컬 환경을 유지한다. 따라서 inner 함수가 정의될 때 x를 참조할 때 사용하는 상위 스코프는 outer 함수의 렉시컬 환경이기 때문에 inner 함수는 outer 함수의x
를 우선적으로 참조하게 된다.
만약, 전역 변수 x를 참조하고 싶다면, inner 함수 내에서 x를 찾지 못하게 outer 함수 내에서 x 변수를 선언하지 않게 되면 상위 스코프인 전역 스코프로 이동하여 전역 변수인 x를 참조할 수 있게 된다.
클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다. 객체의 프라이빗한 상태를 만들고, 외부에서 직접 접근하거나 변경할 수 없도록 하는 것을 의미한다. 즉, 상태를 은닉하고 특정 함수에게만 상태 변경을 허용할 수 있다.
const outer = function () {
const a = 1;
const inner = function () {
return ++a;
}
return inner;
}
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
outer2 변수에 outer 함수가 호출되어 반환된 inner 함수가 할당된다. 이후 outer2()를 호출하면 inner 함수가 실행되고 a값이 1에서 2, 2에서 3으로 증가한다.
캡슐화는 프로퍼티와 메서드를 하나로 묶는 것을 의미한다.
➡️ 데이터와 기능을 함께 그룹화하여 코드의 가독성과 유지 보수성을 향상시키는 데에 도움을 준다.
생성자 함수 내부에 선언된 지역 변수는 외부에서 직접적으로 참조하거나 변경할 수 없다. 이는 지역 변수가 생성자 함수 내부에서만 유효하기 때문이다.
function Counter() {
let count = 0; // 지역 변수
function increase() {
count++;
console.log('Increased count:', count);
}
function decrease() {
if (count > 0) {
count--;
console.log('Decreased count:', count);
}
}
function getCount() {
return count;
}
return {
increase: increase, // 클로저로 감싼 함수를 반환
decrease: decrease,
getCount: getCount
};
}
const counter = Counter();
counter.increase(); // Increased count: 1
counter.increase(); // Increased count: 2
counter.decrease(); // Decreased count: 1
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined
Counter 함수 내부에서 counter 변수와 increase
, decrease
, getCount
함수들이 선언되어 있다. counter 변수와 이 변수에 접근하고 조작하는 함수들이 하나의 단위로 묶여있어 데이터와 기능이 함께 그룹화되어 캡슐화가 이루어진다.
외부에서는 counter 객체를 통해 increase
, decrease
, getCount
메서드에 접근하여 count 변수에 간접적으로 접근하고 조작할 수 있다.
➡️ count
변수가 외부로부터 감춰져 캡슐화되고 해당 변수에 직접적인 접근이 제한되는 효과가 있다.
부분 적용 함수는 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억했다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수를 말한다.
💡 부분 적용 함수의 이점
1. 재사용성 - 원래 함수의 일부 인자를 고정한 후, 해당 부분을 재사용할 수 있다. 그래서 같은 함수를 여러 번 호출하지 않고도 반복되는 코드를 줄일 수 있다.
2. 코드 간소화 - 일부 인자를 미리 고정한 새로운 함수를 생성하여 코드를 간결하고 가독성 좋게 작성할 수 있다.
3. 유연성 - 함수를 더 동적으로 조합하고 활용하라 수 있다.
var add = function () {
console.log(this)
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = add.bind(null, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10)); // 55 출력
bind()
메서드는 첫 번째 인자로 함수가 실행될 때의 this 값을 받고, 그 이후의 인자로는 미리 고정할 인자들을 받습니다.