
모던 자바스크립트 Deep Dive 서적을 참고했습니다.
사실 클로저는 자바스크립트 고유의 개념이 아니다.
클로저의 정의가 ECMAScript사양에 등장하지 않는다.
MDN(신뢰할 수 있는 개발자 문서. 공식문서는 아님) 曰: “클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.”
하... 렉시컬은 또 무엇일까? 이왕 하는 김에 쉽고 빠르게 보고 오자. ⇒ 렉시컬 빠르게 집어넣기 클릭
렉시컬 스코프 개념을 이해했다면 술술 읽힐겁니다.
함수는 '태어난 곳'과 '일하는 곳'이 다를 수 있다. 그래서 함수는 '일하는 곳'과 관계없이 자신이 '태어난 곳'의 환경을 기억해야 하는데, 이때 '태어난 곳'(정의 된 위치, 환경)이 바로 상위 스코프이다. => 기억해야 한다.
다시! 상위 스코프란?: 함수의 정의가 위치하는 스코프
이를 위해 함수는 자신의 내부슬롯[[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.
함수의 [[Environment]]가 어떻게 상위 스코프를 결정하는지 보면:
- 상위 스코프 결정 시점
- 함수 정의가 평가될 때(코드가 실행되기 전) 상위 스코프가 결정된다.
- 이것이 바로 '렉시컬 스코프'.
- [[Environment]]의 역할
- 함수가 자신이 태어난(정의된) 환경을 기억하게 해준다.
자바스크립트 deep dive 24-04
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x);
}
bar함수는 전역에서 정의되었으므로 전역 스코프를 기억한다.- 그래서
bar함수가 어디서 호출되든 상관없이 전역의x값인 1을 참조한다.foo안에서 호출되어도 마찬가지이다.
즉, 함수의 상위 스코프는 함수를 어디서 호출하는지가 아니라, 함수를 어디서 정의했는지에 따라 결정된다. 이것이 바로 렉시컬 스코프의 핵심이다.
이 코드하나 머리에 박아놓고 클로저하면 떠올리기
function foo() {
const x = 1;
const y = 2;
function bar() {
console.log(x); // bar는 foo의 x에 접근 가능
}
return bar;
}
const bar = foo(); // foo를 실행하고 bar 함수를 반환받음
bar(); // bar 실행
생명 주기 관점에서 보기
function outer() {
const name = "철수"; // 원래는 outer 함수 종료시 사라져야 할 변수
function inner() {
console.log(name); // 하지만 inner가 name을 기억함
}
return inner; // inner 함수 반환
}
const savedFunc = outer(); // outer는 실행 종료됨
savedFunc(); // 여전히 "철수" 출력 가능!
- 보통의 경우: outer 함수가 끝나면 그 안의 변수들(name)은 사라져야 함
- 클로저의 경우: inner 함수가 name을 참조하고 있어서 사라지지 않고 유지됨
여기에서 의문이 들 수도 있다! (의문 없다면 pass)
“엥?
outer()함수는 일급객체로써 변수savedFunc에 넣었기때문에savedFunc();를 실행하면outer();가 실행되니까 애초에 이 결과가 당연한거 아니야?” 라고 생각이 들었다면
그렇게 오해할 수 있지만 실제로는 다르게 작동한다.
function outer() {
const name = "박지성";
function inner() {
console.log(name);
}
return inner; // inner 함수를 '반환'
}
// 여기서 잘 보면
const savedFunc = outer(); // (1) outer 함수가 실행되고 inner 함수를 반환
savedFunc(); // (2) 저장된 inner 함수를 실행
const savedFunc = outer()에서 일어나는 일:
savedFunc()에서 일어나는 일:
다시 보자면
// 이렇게 생각이 들 수 있는데
const savedFunc = outer; // outer 함수 자체를 저장
savedFunc(); // outer 함수를 실행
// 실제로는 이렇게 동작함
const savedFunc = outer(); // outer()의 반환값(inner 함수)을 저장
savedFunc(); // 저장된 inner 함수를 실행
비유하자면
outer()는 "조리법"을 주는 게 아니라"일찍 소멸된다"는 의미
function foo() {
const x = 1;
const y = 2;
// 케이스 1: 클로저가 모든 변수 유지
function bar() {
console.log(x, y); // x와 y 모두 필요
}
// 케이스 2: 최적화된 클로저
function optimizedBar() {
console.log(x); // x만 필요
// y는 사용하지 않아서 메모리에서 일찍 제거됨
}
}
이게이게 자바스크립트 엔진의 최적화를 도와준다.
- optimizedBar는 x만 사용하므로 y는 메모리에서 제거
- 불필요한 메모리 점유를 줄임
function counter() {
let count = 0; // 외부에서 직접 접근 불가능한 변수
return {
increase() { count++; },
decrease() { count--; },
getCount() { return count; }
};
}
const myCounter = counter();
myCounter.increase(); // count는 private하게 보호됨
console.log(myCounter.getCount()); // 1
function bigFunction() {
const bigData = new Array(10000); // 큰 데이터
const smallData = "hello"; // 작은 데이터
return function() {
console.log(smallData); // smallData만 사용
// bigData는 사용하지 않으므로 메모리에서 제거됨
};
}
- 최적화 전: bigData와 smallData 모두 메모리 유지
- 최적화 후: smallData만 메모리에 유지, bigData는 일찍 소멸
- 이렇게 클로저는 함수가 자신이 생성된 환경의 변수를 기억하되, 실제로 사용하는 변수만을 메모리에 유지하는 최적화된 방식으로 동작한다.
- 메모리 효율성 & 코드의 캡슐화를 동시 달성!
그래서 자바스크립트에서 클로저가 뭐냐고 물어보면 뭐라고 답해야하지..?!
렉시껄 스코프는 "함수를 어디에 선언했는지"에 따라 상위 스코프를 결정하는
규칙이다.
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x); // 1
}
foo();
클로저는 이 렉시컬 스코프의 규칙을 기반으로, "이미 생명 주기가 끝난 외부 함수의 변수를 참조하는
현상"이다.
function foo() {
const x = 1;
return function bar() {
console.log(x); // 1
}
}
const savedBar = foo(); // foo는 실행 종료
savedBar(); // 하지만 여전히 x에 접근 가능
즉:
- 렉시컬 스코프: 상위 스코프를 결정하는 규칙
- 클로저: 이 규칙을 활용해 이미 종료된 함수의 변수를 참조하는 현상
- 클로저는 렉시컬 스코프라는 규칙이 있기에 가능한 현상이라고 볼 수 있다.