제너레이터는 하나의 값만을 반환하는 함수와 다르게 여러개의 값을 필요에 따라 하나씩 반환할 수 있다.
제너레이터 함수를 만드려면 특별한 문법 구조, function*
이 필요하다.
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
제너레이터는 일반 함수와 다르게 코드가 실행되지 않고 실행을 처리하는 특별한 객체가 반환된다. next()
는 제너레이터의 주요 메서드로 next()
를 호출하면 가장 가까운 yield <value>
문을 만날 때까지 실행이 계속된다. 이후에 yield <value>
문을 만나면 실행을 멈추고 값이 바깥 코드로 반환된다.
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}
next()
는 항상 value
, done
을 가진 객체를 반환한다.
제너레이터 객체는 iterable이다.
.next()
메서드가 있음{value: ..., done: true/false}
이어야 함next() 메서드를 보면서 짐작하셨듯이, 제너레이터는 이터러블 입니다.
제너레이터는 이터러블 객체이므로 for..of
나 전개 문법도 사용할 수 있다.
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
제너레이터 컴포지션은 제너레이터 안에 제너레이터를 임베딩 할 수 있게 해주는 제너레이터의 기능이다.
제너레이터의 특수 문법 yield*
를 사용하면 제너레이터를 다른 제너레이터에 끼워넣을 수 있다.
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);
}
let str = '';
for(let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
yield*를 사용하면 사용한 제너레이터를 대상으로 반복으로 수행하고 산출값들을 밖으로 전달하는 것이다. 제너레이터 컴포지션을 사용하면 제너레이터의 흐름을 다른 제너레이터에 삽입할 수 있다.
yield는 결과를 바깥으로 전달할 뿐 아니라 제너레이터 안으로 전달하기도 한다.
function* gen() {
// 질문을 제너레이터 밖 코드에 던지고 답을 기다립니다.
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
let generator = gen();
let question = generator.next().value; // <-- yield는 value를 반환합니다.
generator.next(4); // --> 결과를 제너레이터 안으로 전달합니다.
제너레이터 안으로 다음과 같이 에러를 전달할 수도 있다.
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)
alert("위에서 에러가 던져졌기 때문에 실행 흐름은 여기까지 다다르지 못합니다.");
} catch(e) {
alert(e); // 에러 출력
}
}
let generator = gen();
let question = generator.next().value;
generator.throw(new Error("데이터베이스에서 답을 찾지 못했습니다.")); // (2)
async iterator를 사용하면 비동기적으로 들어오는 데이터를 필요에 따라 처리할 수 있다.
이터러블 객체를 비동기적으로 만드려면
for await (let item of iterable)
반복문을 사용해 처리해야한다.let range = {
from: 1,
to: 5,
// for await..of 최초 실행 시, Symbol.asyncIterator가 호출됩니다.
[Symbol.asyncIterator]() { // (1)
// Symbol.asyncIterator 메서드는 이터레이터 객체를 반환합니다.
// 이후 for await..of는 반환된 이터레이터 객체만을 대상으로 동작하는데,
// 다음 값은 next()에서 정해집니다.
return {
current: this.from,
last: this.to,
// for await..of 반복문에 의해 각 이터레이션마다 next()가 호출됩니다.
async next() { // (2)
// next()는 객체 형태의 값, {done:.., value :...}를 반환합니다.
// (객체는 async에 의해 자동으로 프라미스로 감싸집니다.)
// 비동기로 무언가를 하기 위해 await를 사용할 수 있습니다.
await new Promise(resolve => setTimeout(resolve, 1000)); // (3)
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
(async () => {
for await (let value of range) { // (4)
alert(value); // 1,2,3,4,5
}
})()
일반 제너레이터에서는 await
를 사용할 수 없다. 그런데 제너레이터의 본문에서 await
를 사용해야하는 상황이 발생하면 제너레이터 앞에 async를 붙여주면 된다.
그러면 for await...of
로 반복이 가능한 async 제너레이터를 사용할 수 있게 된다.
async 제너레이터의 next() 메서드는 비동기적으로 프로미스를 반환한다.