클로저에 대한 정의
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합입니다. - MDN
함수가 선언된 렉시컬 환경
const x = 1;
function outerFunc() {
const x = 10
function innerFunc() {
console.log(x) // 10
}
innerFunc()
}
outerFunc()
중첩함수 innerFunc의 상위 스코프는 outerFunc의 스코프 -> 그래서 x = 10에 [먼저] 접근하는 것
만약 이렇다면? innerFunc()에서 출력하는 x값은 뭘까?
const x = 1
// innerFunc()에서는 outerFunc()의 x에 접근할 수 없다.
// Lexical Scope를 따르는 프로그래밍 언어이기 때문이다.
function outerFunc() {
const x = 10
innerFunc()
}
function innerFunc() {
console.log(x) // 1
}
outerFunc()
위 예시처럼 innerFunc 함수를 outerFunc 함수의 내부에서 호출한다 하더라도 outerFunc 함수의 변수에 접근할 수는 없어요.
렉시컬 스코프
i. JS엔진은 함수를 어디서 "호출했는지"가 아니라 함수를 어디에 "정의했는지"에 따라 상위 스코프를 결정
const x = 1
function foo() {
const x = 10
bar()
}
function bar() {
console.log(x)
}
foo() // 1
bar() // 1
ii. 다시 말하면, "외부 렉시컬 환경에 대한 참조"에 저장할 참조값, 즉, 스코프에 대한 참조는 함수 정의가 평가되는 시점에
함수가 정의된 환경(위치)에 의해 결정된다. 이것이 바로 렉시컬 스코프다.
정의된 환경에 대한 정보를 저장하는 곳 : outer
i. 함수가 정의된 환경(위치)과 호출된 환경(위치)은 다를 수 있다. 따라서 호출되는 환경과는 상관없이 정의된 환경에 대한 정보를 기억한다.
어디에 ? -> LexicalEnvironment > outer
const x = 1
function foo() {
const x = 10
// 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
// 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
bar()
}
// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 [[Environment]]에 저장하여 기억한다.
function bar() {
console.log(x)
}
foo() // 1
bar() // 1
클로저와 렉시컬 환경(LexicalEnvironment)
i. 외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 (여전히) 참조할 수 있다.
이 개념에서 중첩 함수가 바로 클로저
const x = 1
// 1
function outer() {
const x = 10
const inner = function () { console.log(x) }
return inner
}
const innetFunc = outer()
innerFunc()
// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스탭에서 팝되어 제거된다(역할을 다 했으니깐)
// inner 함수는 런타임에 평가된다.
// inner 함수가 innerFunc에 전달되었는데, 이는 outer 함수의 렉시컬환경을 참조하고 있다.
// 즉, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다.
똑똑한 가비지 컬렉터
- 안쓰는 것만 가져간다~ 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()
클로저의 활용
i. 클로저 어려운데 왜씀?
- 클로저는 JS의 강력한 기능이므로, 필요하다면 적극 활용해야 한다. 클로저가 유용하게 사용되는 상황을 봅시다.
ii. 클로저는 주로 "상태를 안전하게 변경하고 유지하기 위해 사용"한다. 의도치 않은 상태의 변경을 막기 위해서!
iii. 상태를 안전한게 은닉한다(특정 함수에게만 상태 변경을 허용한다)는 표현을 기억하자.
- 이 표현으로 사실 closure라는 용어의 의미를 좀 더 깊숙히 들여다볼 수 있어요!
iv. 예제(카운터)
// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터
// 카운트 상태 변수
let num = 0
// 카운트 상태 변경 함수
const increase = function() {
// 카운트 상태를 1만큼 증가시킨다.
}
console.log(increase())
// num = 100 // 치명적인 단점이 있다.
console.log(increase())
console.log(increase())
// 보완해야 할 사항
// (1) 카운트 상태 (num 변수의 값)는 increase 함수가 호출되기 전까지 변경되지 않고 유지돼야 한다.
// (2) 이를 위해 카운트 상태 (num 변수의 값)는 increase 함수만이 변경할 수 있어야 한다.
// 전역변수 num이 문제다 -> 지역변수로 바꿔볼까?
// 카운트 상태 변경 함수
const increase = function() {
// 카운트 상태 변수
let num = 0
// 카운트 상태를 1만큼 증가시킨다.
return ++num
}
// 이전 상태값을 유지 못함
console.log(icrease()) // 1
console.log(icrease()) // 1
console.log(icrease()) // 1
// num 변수는 increase 함수의 지역변수로 선언됐기 때문에 의도치 않는 변경은 방지!
// 즉, num 변수의 상태는 increase 함수만이 변경할 수 있다.
// increase()가 호출될 때마다 num이 초기화
// 백번, 천번 호출해도 언제 1인 increase...
// 의도치 않은 변경은 방지하면서, 이전 상태를 유지하기 위해 클로저 사용
// 카운트 상태 변경 함수
const increase = (function () {
// 카운트 상태 변수
let num = 0
// 클로저
return function () {
return ++num
}
}())
// 이전 상태값을 유지
console.log(increase()) // 1
console.log(increase()) // 2
console.log(increase()) // 3
// 코드 설명
// 1. 위 코드가 실행되면 즉시 실행 함수가 호출되고 즉시 실행 함수가 반환한 함수가
// increase 변수에 할당된다.
// 2. increase 변수에 할당된 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프인
// 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저다.
// 3. 즉시 실행 함수는 호출된 이후 소멸되지만, 즉시 실행 함수가 반환한 클로저는 increase
// 변수에 할당되어 호출된다.
// 4. 이때 즉시 실행 함수가 반환한 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프인
// 즉시 실행 함수의 렉시컬 환경을 기억하고 있다.
// 5. 따라서 즉시 실행 함수가 반환한 클로저는 카운트 상태를 유지하기 위한 자유 변수 num을
// 언제 어디서 호출하든지 참조하고 변경할 수 있다.
// 6. num은 초기화되지 않을 것이며, 외부에서 직접 접근할 수 없는 은닉된 private 변수이므로,
// 전역 변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요도 없다.
// 결론 : 클로저는 상태(state)가 의도치 않게 변경되지 않도록 안전하게 은닉(information hiding)
// 하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경
// 하고 유지하기 위해 사용한다.
// 클로저 카운트 기능 확장(값 감소 기능 추가)
const counter = (fucntion() {
// 카운트 상태 변수
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
// 8번째 줄에서 { 가 있는데, 별도의 스코프를 생성하진 않나요?
함수형 프로그래밍의 특징
- 외부 상태 변경이나 가변 데이터를 피하고, 불변셩을 지향
- 외부 상태의 변경이 쉽게 일어날 경우, 프로그램 오류의 근본적인 원인이 될 수 있음
- 클로저를 활용하여, 예상치 못한 외부 영향 오류를 피하고 프로그램의 안정성을 높일 수 있음
- 함수형 프로그래밍에서 클로저를 활용하는 예시
// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
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 increase() = makeCounter(increase)
console.log(increaser())
console.log(increaser())
// increase 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동되지 않는다.
const decrease = makeCounter(decreaser)
console.log(decreaser())
console.log(decreaser())
// 그럼 어떻게 해야 하나요?
// -> key : makeCounter 함수를 1번만 호출할 것
// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
const counter = (function() {
// 카운트 상태를 유지하기 위한 변수
let counter = 0
// 클로저를 반환
return function (aux) {
// 인수로 전달받은 보조 함수에 상태 변경을 위임한다.
counter = aux(counter)
return counter
}
}())
// 보조 함수
function increase(n) {
return ++n
}
// 보조 함수
function decrease(n) {
return --n
}
// 함수로 함수를 생성
// makeCounter함수는 보조 함수를 인수로 전달받아 함수를 반환한다.
console.log(counter(increase))
console.log(counter(increase))
// 자유 변수를 공유
console.log(counter(decrease))
console.log(counter(decrease))
// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
const counter = (function() {
//카운트 상태를 유지하기 위한 자유 변수
let counter = 0
// 클로저를 반환
return function (aux) {
// 인수로 전달받은 보조 함수에 상태 변경을 위임한다.
counter = aux(counter)
return counter
}
}())
// 보조 함수
function increase(n) {
return ++n
}
// 보조 함수
function decrease(n) {
return --n
}
// 함수로 함수를 생성
// makeCounter 함수는 보조 함수를 인수로 전달받아 함수를 반환한다.
console.log(counter(increase))
console.log(counter(increase))
// 자유 변수를 공유
console.log(counter(decrease))
console.log(counter(decrease))
캡슐화와 정보 은닉
캡슐화란 ?
i. 프로퍼티와 메서드를 하나로 묶는 것
- 프로퍼티: 객체의 상태(state)
- 메서드: 프로퍼티를 참조하고 조작할 수 있는 동작(behavior)
ii. 객체의 특징 프로퍼티나 메서드를 감출 목적으로 사용 --> 정보 은닉(information hiding)
- 객체의 상태 변경을 방지함으로써 정보 보호
- 객체 간의 의존성 (결합도 - coupling)을 낮춤
iii. java 등 기타 객체지향 언어에서 사용하는 public, private, protected ...
iv. 자바스크립트는 제공 안해요.
- 즉, 별도의 조치를 취하지 않으면 기본적으로 외부 공개가 된다는 의미
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("lee", 27)
me.sayHi() // Hi! My name is lee. I am 27.
console.log(me.name) // lee
console.log(me._age) // undefined
const you = new Person("leo", 26)
you.sayHi() // Hi My name is leo. I am 26.
console.log(you.name) // leo
console.log(you.age) // undefined
자주 발생하는 실수
다음 코드를 살펴보자
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
'JavaScript' 카테고리의 다른 글
[JS] 순수함수 (0) | 2023.04.06 |
---|---|
[JS] Promise란 무엇인가? (0) | 2023.03.20 |
[JS] 콜백함수 정리 (0) | 2022.12.01 |
[JS] this 에 대한 모든 것 (0) | 2022.12.01 |
[JS] 실행 컨텍스트란? (Execution Context) (0) | 2022.11.30 |