제너레이터란?
function *
문법으로 생성할 수 있는 특수한 함수이다.- 작업 중간에 일시 정지가 가능하다.
- 멋진 점은 일시정지된 상태로 진행된 작업 정보를 기억하고 있는 것이다.
- 값을 생성하고 선택적으로 새 값을 받아들인 다음 필요한 만큼 계속 진행할 수 있다.
- 내부적으로 제너레이터 함수는 제너레이터 객체를 만들고 반환한다.
- 이터레이터는 값만 생성하는 반면, 제너레이터는 값을 생성하고 소비할 수 있다.
- 단방향으로 값을 주기만하는 것이 아니라, 값을 받아들이고 그에 따른 값을 주는 것이 가능하다.
- 제너레이터 객체를 수동으로 생성하는 것도 가능하지만 보통은
function *
문법으로 단순화하여 생성한다.
기본 제너레이터 함수 생성하기
function* simple() {
for (let n = 1; n <= 3; ++n) {
yield n;
}
}
const it = simple();
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
for (const e of simple()) {
console.log(e); // 1, 2, 3
}
console.log(it[Symbol.iterator]());
/*
simple {<suspended>}
[[GeneratorLocation]]: VM43:1
[[Prototype]]: Generator
[[GeneratorState]]: "suspended"
[[GeneratorFunction]]: ƒ* simple()
[[GeneratorReceiver]]: Window
*/
simple()
의 결과는 제너레이터 객체이다.next()
메서드를 수행하면,yield
로 값을 반환할 때까지 코드를 수행한다.yield
를 만나지 못하면,{value: undefined, done: true}
를 반환한다.
제너레이터로 iterator
구현하기
const a = {
0: "a",
1: "b",
2: "c",
length: 3,
[Symbol.iterator]: function* () {
for (let index = 0; index < this.length; ++index) {
yield this[index];
}
},
};
for (const value of a) {
console.log(value);
}
- 제너레이터를 사용하지 않고 구현하는 것보다 훨씬 간단하다.
LinkedList
클래스의 iterator
를 제너레이터 이용해서 구현해보기
class LinkedList {
constructor() {
this.head = this.tail = null;
}
add(value) {
const entry = { value, next: null };
if (!this.tail) {
this.head = this.tail = entry;
} else {
this.tail = this.tail.next = entry;
}
}
*[Symbol.iterator]() {
for (let cur = this.head; cur !== null; cur = cur.next) {
yield cur.value;
}
}
}
const list = new LinkedList();
list.add("one");
list.add("two");
list.add("three");
for (const e of list) {
console.log(e);
}
*[Symbol.iterator](){ ... }
메서드명 앞에*
을 붙여주는 방법을 사용할 수 있다.
제너레이터로 값 소비하기
- 제너레이터는 이터레이터와 다르게 '값을 소비할 수 있다.'
- 값을 소비한다는 것은
next()
메서드에 값을 받아 로직에 이용한다는 뜻이다.
function* tellMeAboutYou() {
const name = yield "what is your name?";
console.log(`oh your name is ${name}`);
const age = yield "how old are you?";
console.log(`oh now you are ${age}`);
}
const gt = tellMeAboutYou();
console.log(gt.next());
// what is your name?
console.log(gt.next("jake seo"));
// oh your name is jake seo
// how old are you?
gt.next("5");
// oh now you are 5
- 처음부터 무언가 값을 넘기고 싶다면, 제너레이터 함수의 인자를 이용하자.
제너레이터의 용도
- 기본적으로 상태머신이다.
- 사용자로부터 값을 무한대로 입력받고, 무조건 마지막 세 수의 합을 구하는 경우 유용하다.
- 심리테스트 등 사용자가 입력한 정보 값을 기반으로 다음 값을 결정해야 하는 경우 유용하다.
function* sumLastThree() {
let arr = [];
while (true) {
if (arr.length < 3) {
const num = yield `input ${3 - arr.length} more numbers`;
arr.push(num);
continue;
}
const num = yield arr.reduce((a, c) => a + c, 0);
arr = [...arr.slice(-2), num];
}
}
const gt = sumLastThree();
console.log(gt.next()); // {value: 'input 3 more numbers', done: false}
console.log(gt.next(1)); // {value: 'input 3 more numbers', done: false}
console.log(gt.next(2)); // {value: 'input 3 more numbers', done: false}
console.log(gt.next(3)); // {value: 6, done: false}
console.log(gt.next(4)); // {value: 9, done: false}
console.log(gt.next(100)); // {value: 107, done: false}
제너레이터의 return
function* usingReturn() {
yield 1;
yield 2;
return 3;
}
for (const v of usingReturn()) {
console.log(v);
}
// 1
// 2
// NOT LOGGED -> {value: 3, done: true}
- 제너레이터의
return
은{done: true}
인 오브젝트를 반환한다. for-of
문은done: true
인 경우의value
는 보지 않기 때문에, 출력되지 않는다.
yield
의 낮은 우선 순위
function* example() {
let a = yield +2 + 30;
return a;
}
yield
의 낮은 우선순위 때문에,yield
로 받은 값에2
와30
이 더해지는 것이 아니라,+2+30
이 먼저 계산되고let a = yield 32
와 같이 해석된다.
function* example() {
let a = yield* 2 + 30;
return a;
}
*2+30
은 올바른 연산이 아니므로 에러가 발생한다.
function* example() {
let a = yield 2 + 30 + yield;
return a;
}
- 아예 문법 에러가 난다.
function* example() {
let a = (yield) + 2 + 30;
return a;
}
- 괄호에 있는 것이 먼저 해석되기 때문에, 이는 올바른 문법이다.
yield
의 해석을 먼저 진행한다.
제너레이터의 throw
function* example() {
yield 1;
yield 2;
yield 3;
}
const gen = example();
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.throw(new Error("boom"))); // Uncaught Error: boom
console.log(gen.next()); // not executed.
function* example() {
while (true) {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.error("generator boom!");
yield "error";
}
}
}
const gen = example();
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.throw(new Error("boom"))); // {value: 'error', done: false}
console.log(gen.next()); // {value: 1, done: false}
제너레이터와 이터러블 넘겨주기: yield*
function* collect(count) {
const data = [];
if (count < 1 || Math.floor(count) !== count) {
throw new Error("count must be an integer >= 1");
}
do {
let msg = "values needed: " + count;
data.push(yield msg);
} while (--count > 0);
return data;
}
function* outer() {
let data1 = yield* collect(2);
console.log("data collected by collect(2) =", data1);
let data2 = yield* collect(3);
console.log("data collected by collect(3) =", data2);
return [data1, data2];
}
const outerG = outer();
console.log("next got:", outerG.next());
console.log("next got:", outerG.next("a"));
console.log("next got:", outerG.next("b"));
console.log("next got:", outerG.next("c"));
console.log("next got:", outerG.next("d"));
console.log("next got:", outerG.next("e"));
yield*
키워드를 통해 제너레이터를 넘겨주었다.- 이를 이용해 내부의 다른 제너레이터 상태머신을 이용할 수 있다.
내부 제너레이터 실행 중 return
하기
function* inner() {
try {
let n = 0;
while (true) {
yield "inner " + n++;
}
} finally {
console.log("inner terminated");
}
}
function* outer() {
try {
yield "outer before";
yield* inner();
yield "outer after";
} finally {
console.log("outer terminated");
}
}
const gen = outer();
let result = gen.next();
console.log(result);
result = gen.next();
console.log(result);
result = gen.next();
console.log(result);
result = gen.return(42);
console.log(result);
result = gen.next();
console.log(result);
/*
{value: 'outer before', done: false}
{value: 'inner 0', done: false}
{value: 'inner 1', done: false}
inner terminated
outer terminated
{value: 42, done: true}
{value: undefined, done: true}
*/
return
예제 2: 바로 return
하면?
function foo() {
try {
return "a";
} finally {
return "b";
}
}
console.log(foo()); // b
return
을 하는 순간finally()
로 넘어가기 때문에 바로"b"
가 반환된다.
return
예제 3: return
으로 값 오버라이드
function* foo(n) {
try {
while (true) {
n = yield n * 2;
}
} finally {
return "override";
}
}
const gen = foo(2);
console.log(gen.next()); // {value: 4, done: false}
console.log(gen.next(3)); // {value: 6, done: false}
console.log(gen.next(4)); // {value: 8, done: false}
console.log(gen.return(4)); // {value: "override", done: true}
throw
해보기
function* inner() {
try {
yield "something";
console.log("inner - done");
} finally {
console.log("inner - finally");
}
}
function* outer() {
try {
yield* inner();
console.log("outer - done");
} finally {
console.log("outer - finally");
}
}
const gen = outer();
let result = gen.next();
result = gen.throw(new Error("boom"));
/*
inner - finally
outer - finally
Uncaught Error: boom
*/
반응형
'자바스크립트 > 모던 자바스크립트' 카테고리의 다른 글
모던 자바스크립트, 비동기 버전의 이터레이터, 이터러블, 제너레이터 (0) | 2023.02.28 |
---|---|
모던 자바스크립트, async await (0) | 2023.02.28 |
모던 자바스크립트, 이터러블 (iterable) 과 이터레이터 (iterator) (0) | 2023.02.28 |
모던 자바스크립트, 프라미스 2 - 유틸 메서드와 작업 패턴 그리고 안티 패턴 (0) | 2023.02.15 |
모던 자바스크립트, 프라미스 1 - 기본 개념 (0) | 2023.02.15 |