일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- Kubernetes
- node.js
- bean
- 인접행렬
- css
- Deep Dive
- html
- typescript
- winston
- 프로그래머스
- 코딩테스트
- REST API
- Interceptor
- TIL
- GraphQL
- MySQL
- JWT
- Spring
- 인접리스트
- nestjs
- puppeteer
- 탐욕법
- javascript
- dfs
- 자료구조
- Linux
- java
- OOP
- 알고리즘
- LifeCycle
- Today
- Total
처음부터 차근차근
[코어 자바스크립트] Closure 본문
Closure에 대한 정의
: A closure is the combination of a function and the lexical environment within which that function was declared(클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합니다.) - MDN
이걸 알기 위해서는 함수가 선언된 렉시컬 환경에 대하여 자세하게 알 필요가 있다.
아래의 예시를 보자.
const x = 1;
function outerFunc() {
const x = 10;
function innerFunc() {
// x는 어디서 참조할까요??
// 함수가 선언된 렉시컬 환경!!!
// 함수가 선언될 당시의 외부 변수 등의 정보!
console.log(x); // 10
}
innerFunc();
}
outerFunc();
흐름을 살펴보면
- innerFunc() 내부의 console.log(x)에서 참조하고 있는 x 값은
- 먼저 스코프 내부에서 x 값을 찾는다.
- 없는 경우 scope chain에 의해 바로 바깥쪽 scope를 찾는다.
- 실행컨텍스트에서 배웠던 outer를 찾는 것
- outer는 해당 실행컨텍스트의 생성시점의 LexicalEnvironment를 갖고 있다
- 그래서 10에 먼저 접근하고, console.log(x)는 10이 출력!
만약 아래의 코드와 같다면??
둘 다 1이 출력된다.
// [렉시컬 스코프]
// JS 엔진은 함수를 어디서 '호출'했는지가 아니라
// 어디에 '정의'했는지에 따라서 스코프(상위 스코프)를 결정한다.
// '외부 렉시컬 환경에 대한 참조값' => 실행 컨텍스트의 outer
// 함수 정의가 평가되는 시점!!! Not 호출!!
const x = 1;
// outerFuck 내에 innerFunc가 '호출' 되고 있음에도 불구하고.
// innerFunc()에서는 outerFunc()의 x에 접근할 수 없죠.
// Lexical Scope를 따르는 프로그래밍 언어이기 때문
function outerFunc() {
const x = 10;
innerFunc(); // 1
}
// innterFunc와 outerFunc
// 각각 다른 Scope를 가지고 있다.
function innerFunc() {
console.log(x); // 1
}
outerFunc();
렉시컬 스코프에 대해 좀 더 자세히 알아보자.
- JS엔진은 함수를 어디서 '호출했는지'가 아니라, 함수를 어디에 '정의했는지'에 따라 상위 스코프를 결정.
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // 1
bar(); // 1
→ 다시 말하면, '외부 렉시컬 환경에 대한 참조'에 저장할 참조값, 즉 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다. = 렉시컬 스코프
그럼 정의된 환경에 대한 정보를 저장하는 곳은?? : outer
const x = 1;
function foo() {
const x = 10;
// 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
// 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
bar();
}
// 여기보세요 여기!
// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 저장하여
// 기억한다.
function bar() {
console.log(x);
}
foo();
bar();
클로저와 렉시컬 환경
외부 함수보다 중접 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 (여전히) 참조할 수 있다. ← 이 개념에서 중첩 함수가 바로 클로저이다.
const x = 1;
// 1
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
// outer함수를 실행해서 innerFunc에 담는다.
// outer return 부분을 innerFunc에 담는다.
const innerFunc = outer();
// -------------------------- 여기서는.. outer함수의 실행컨텍스트는??
innerFunc();
// 변화해보자.
// const innerFunc = function () {
// console.log(x);
// };
- outer 함수를 호출하면 중첩 함수 inner를 반환(return)해요.
- 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스탭에서 팝되어 제거된다(역할을 다 했으니깐)
- inner 함수는 런타임에 평가된다.
- inner함수가 innerFunc에 전달되었는데, 이는 outer 함수의 렉시컬환경을 (여전히) 참조하고 있다.
- 즉, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다
어떻게 이게 가능한가?? -> 가비지 컬렉터는 참조 카운트가 0인 렉시컬 환경만 가져가기 때문!
클로저와 클로저가 아닌 것을 구분해보자.
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();
클로저의 활용
클로저는 주로 '상태를 안전하게 변경하고 유지하기 위해 사용'한다. 의도치 않은 상태의 변경을 막기 위해.
→ 상태를 안전하게 은닉한다(특정 함수에게만 상태 변경을 허용한다)는 표현
// 카운트 상태 변경 함수 #1
// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터를 구현해요!
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
console.log(increase()); // 1
num = 100; // 치명적인 단점이 있어요.
console.log(increase()); // 101
console.log(increase()); // 102
// 보완 사항
// 1 카운트 상태(num 변수의 값) => increase 함수가 호출되기 전까지는 변경되면 안됨.
// 2. 카운트 상태는 increase 함수만이 변경할 수 있어야 한다.
// 3. 전역변수 num 이놈이 문제다. -> 지역변수로??
// 카운트 상태 변경 함수 #2
const increase = function () {
// 카운트 상태 변수
let num = 0;
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
// 이전 상태값을 유지 못함
console.log(increase()); //1
console.log(increase()); //1
console.log(increase()); //1
// 리뷰
// 1.num 변수는 increase 함수의 지역 변수로 선언 -> 전역에서 변경 방지
// = num 변수는 increase 함수만 변경할 수 있었음
// 하지만 increase가 호출될 때마다 num이 초기화되는 이상한 코드
// 의도치 않은 변경은 방지하면서, 이전 상태를 유지해야 함!
// 클로저를 사용해보자!
// 카운트 상태 변경 함수 #3
const increase = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
return ++num;
};
})();
//
// 이전 상태값을 유지
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3
// 위 코드 실행되면, '즉시 실행함수'가 호출!!
// -> 함수가 반환(inner) -> increase에 할당
// 2. increase변수에 할당된 함수는 자신의 정의된 위치에서 의해서 결정된 상위 스코프인
// 즉시 실행 함수의 ' 렉시컬 환경'을 기억하는 클로져 --> let num = 0;을 기억한다
// 3. 즉시 실행 함수는 -> 즉시 소멸된다!!(outer 함수가 불리자마자 바로 call stack 에서 popup 되는 것과 비슷!!)
// * 결론 : num은 초기화 X, 외부에서 접근할 수 없는 은닉된 값!! 의도되지 않은 변경도 걱정할 필요가 없다.
// --> increase에서만 변경할 수 있기 때문에..!!
2. 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때
3. 부분 적용 함수
부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자를 넘겨 기억시켰다가, 나중에 (n-m)넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다.
var partial = function() {
var originalPartialArgs = arguments;
var func = originalPartialArgs[0];
if (typeof func !== 'function') {
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function() {
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
var restArgs = Array.prototype.slice.call(arguments);
return func.apply(this, partialArgs.concat(restArgs));
};
};
var add = function() {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = partial(add, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10)); // 55
var dog = {
name: '강아지',
greet: partial(function(prefix, suffix) {
return prefix + this.name + suffix;
}, '왈왈, '),
};
dog.greet('입니다!'); // 왈왈, 강아지입니다.
4. 커링 함수
커링 함수 란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말합니다. 커링은 한 번에 하나의 인자만 전달하는 것을 원칙으로 합니다.
var curry5 = function(func) {
return function(a) {
return function(b) {
return function(c) {
return function(d) {
return function(e) {
return func(a, b, c, d, e);
};
};
};
};
};
};
var getMax = curry5(Math.max);
console.log(getMax(1)(2)(3)(4)(5));
이러한 콜백지옥을 막기 위해서 ES6 화살표 함수를 쓰면 한 줄로 나타낼 수 있다.
var curry5 = func => a => b => c => d => e => func(a,b,c,d,e);
화살표 순서에 따라 함수에 값을 차례로 넘겨주면 마지막에 func가 호출될 거라는 흐름이 한 눈에 파악된다.
함수형 프로그래밍에서의 지연 실행 : 당장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 하면 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 것
'Language > JavaScript' 카테고리의 다른 글
[Deep Dive] 4. 변수 (1) | 2023.10.26 |
---|---|
[Javascript] 자바스크립트 동작 원리 (1) | 2023.10.25 |
[코어 자바스크립트] Class (0) | 2023.06.12 |
Javascript DOM (0) | 2023.06.12 |
[코어 자바스크립트] Callback 함수 & 동기/비동기 처리 (1) | 2023.06.11 |