일반적인 함수는 하나의 값을 반환하거나 반환하지 않는다.
제너레이터
는 여러 개의 값을 필요에 따라 하나씩 반환(yield
)할 수 있다.
제너레이터와 이터러블 객체를 함께 사용하면 데이터 스트림을 만들 수 있다.
제너레이터 함수는 일반 함수와 동작 방식이 다른데 제너레이터 함수를 호출하면 코드가 실행되지 않고, 대신 실행을 처리하는 특별 객체인 제너레이터 객체
가 반환된다.
// 제너레이터 객체 반환 예시
function* exampleGenerator() {
yield 1
yield 2
yield 3
}
let generator = exampleGenerator();
console.log(generator) // [object Generator]
제너레이터는 yield
라는 생소한 단어가 존재한다. 이는 제너레이터 함수의 실행을 일시적으로 정지시키며, 뒤에 오는 표현 식은 제너레이터의 caller에게 반환되는데 일반 함수의 return
과 매우 유사하다고 볼 수 있다.
여기서 제너레이터 함수는 Callee이고, 이를 호출하는 함수가 Caller이며, Caller는 Callee의 yield
부분에서 다음 statement로 진행을 할 지 여부를 제어한다.
제너레이터는 next()를 통해 결과값을 받을 수 있는데 모든 yield
를 처리하기 위해 그만큼의 next를 사용해야하는 가?에 대한 답은 그럴 수도 있고, 아닐 수도 있다이다.
next
를 일일이 호출하지 않고 yield
를 모두 처리하려면 다음과 같이 재귀 호출을 하면 된다.
// 홀수는 그대로 출력하고, 짝수에는 1을 더하여 출력하는 Code
function* generator() {
console.log(yield 10)
console.log(yield 15)
console.log(yield 7)
}
function run(gen) {
const it = gen();
(function iterator({value, done}) {
if (done) {
return value
}
if (value % 2 === 0) {
return iterator(it.next(value + 1))
} else {
return iterator(it.next(value))
}
})(it.next())
}
// 11
// 15
// 7
제너레이터에는 next
외에도 throw
, return
등의 메소드가 있는데 종료되는 방법의 차이가 있다.
next()
:return(value)
:throw(exception)
:Symbol.iterator 대신 제너레이터 함수를 사용하면, 제너레이터 함수로 반복이 가능합니다.
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // [Symbol.iterator]: function*()를 짧게 줄임
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
alert( [...range] ); // 1, 2, 3, 4, 5
제너레이터 특성상 값을 순회하거나 여러 개를 순차적으로 출력하게 되는 데, 이로 인해 계산의 결과 값이 필요할 때까지 계산을 늦출 수 있다. 이것을 지연평가
라고 한다.
지연평가는 아래와 같은 3가지 이점을 가지고 있다.
1. 불필요한 계산을 하지 않으므로 빠른 계산이 가능하다.
2. 무한 자료 구조를 사용할 수 있다.
3. 복잡한 수식에서 오류 상태를 피할 수 있다.
이러한 장점으로 일반 함수와 제너레이터 함수 간의
배열을 만드는 속도 비교를 했을 때 아래와 같이 크게 차이 나는 경우가 생긴다.
이는 배열의 크기가 클 수록 더 벌어진다.
function makeArr(n) {
let i = 1;
const res = [];
while (i < n) res.push(i++);
return res;
}
function* makeArrGenerator(n) {
let i = 1;
while (i < n) yield i++;
}
function getDivideToFive(iter) {
const res = [];
for (const item of iter) {
if (item % 5 == 0) res.push(item);
}
return;
}
console.time('1');
console.log(getDivideToFive(makeArr(10000)));
console.timeEnd('1');
// 7.69ms
console.time('2');
console.log(getDivideToFive(makeArrGenerator(10000)));
console.timeEnd('2');
// 3.857ms
yield
에 *
을 사용했을 때 경우에 따라 아래 2가지 기능을 할 수 있다
// 1. 이터러블 순회 예시
function* iterableYield() {
const a = 1;
yield a;
yield* [1, 2, 3].map(el => el * (10 ** a));
const b = 2;
yield b;
yield* [1, 2, 3].map(el => el * (10 ** b));
const c = 3;
yield c;
yield* [1, 2, 3].map(el => el * (10 ** c));
}
function run(gen) {
const it = gen();
(function iterate({ value, done }) {
console.log({ value, done });
if (done) {
return value;
}
iterate(it.next(value));
})(it.next());
}
run(iterableYield);
// { value: 1, done: false }
// { value: 10, done: false }
// { value: 20, done: false }
// { value: 30, done: false }
// { value: 2, done: false }
// { value: 100, done: false }
// { value: 200, done: false }
// { value: 300, done: false }
// { value: 3, done: false }
// { value: 1000, done: false }
// { value: 2000, done: false }
// { value: 3000, done: false }
// { value: undefined, done: true }
// 2. 다른 제너레이터 함수 실행 예시
function* innerGenerator(arr) {
yield* ['a', 'b', 'c', 'd'].map((each, idx) => {
let obj = {}
obj[each] = arr[idx]
console.log(obj)
});
}
function* generator() {
arr = [1, 2, 3]
yield* innerGenerator(arr);
}
[...generator()];
// { a: 1 }
// { b: 2 }
// { c: 3 }
// { d: undefined }
제너레이터와 이터레이블 개념 자체는 크게 어렵진 않았지만 실무에 적재적소에 활용하기엔 깊은 이해와 많은 연습이 필요할 것 같다!!