A generator is a function that can pause in the middle and resume later. Each time it pauses, it hands out a value. That makes it the easiest way to build an iterator: instead of writing next() by hand, you write an ordinary-looking function with yield.
Put a star after function to make a generator. Inside, yield hands out a value and pauses:
function* countToThree() {
yield 1;
yield 2;
yield 3;
}
const gen = countToThree(); // nothing runs yet!
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
next().
function* steps() {
console.log(" start");
yield "A";
console.log(" resumed");
yield "B";
console.log(" finishing");
}
const g = steps();
console.log(g.next().value);
console.log(g.next().value);
console.log(g.next().done);
Each next() runs the function from where it last paused up to the next yield, then freezes it again, keeping all its local variables intact.
Because a generator object is an iterator (and an iterable), every iteration tool accepts it:
function* range(from, to, step = 1) {
for (let n = from; n <= to; n += step) {
yield n;
}
}
for (const n of range(1, 5)) console.log(n);
console.log([...range(0, 20, 5)]);
Compare that to the hand-written range object in the previous lesson: same result, a fraction of the code.
const playlist = {
songs: ["Intro", "Anthem", "Outro"],
*[Symbol.iterator]() {
for (const s of this.songs) yield `♪ ${s}`;
}
};
console.log([...playlist]);
A generator only does work when asked for the next value, so it can describe an endless sequence safely. You just stop asking:
function* idMaker() {
let id = 1000;
while (true) {
yield id++;
}
}
const ids = idMaker();
console.log(ids.next().value); // 1000
console.log(ids.next().value); // 1001
console.log(ids.next().value); // 1002
break. [...idMaker()] tries to collect every value forever and freezes the page.
// Safe: take only what you need
function take(iterable, n) {
const out = [];
for (const x of iterable) {
if (out.length === n) break;
out.push(x);
}
return out;
}
console.log(take(idMaker(), 4)); // [1000, 1001, 1002, 1003]
Generator objects also have the iterator helpers from the previous lesson, so you can use the built-in take() instead of writing one:
console.log(idMaker().take(4).toArray());
console.log(idMaker().filter(n => n % 2 === 0).map(n => `ID-${n}`).take(3).toArray());
Put take() before toArray(). idMaker().toArray().slice(0, 4) never finishes, because toArray() tries to collect the whole endless sequence first.
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
console.log(take(fibonacci(), 10));
function* untilStop(words) {
for (const w of words) {
if (w === "STOP") return;
yield w;
}
}
console.log([...untilStop(["go", "on", "STOP", "never"])]); // ["go", "on"]
A generator can return a value too. next() hands it out with done: true, but for...of and spread stop as soon as they see done: true, so they never use it:
function* myStream() {
yield 1;
yield 2;
return 3;
}
console.log([...myStream()]);
const g = myStream();
console.log(g.next(), g.next(), g.next(), g.next());
If you want a value to be part of the sequence, yield it. Use return only to stop.
Every generator object has three methods:
| next() | Runs to the next yield and returns { value, done } |
| return(value) | Finishes the generator right now, as if a return ran at the paused spot |
| throw(error) | Throws an error at the paused spot inside the generator |
function* count() {
yield 1;
yield 2;
yield 3;
}
const c = count();
console.log(c.next());
console.log(c.return("bye"));
console.log(c.next()); // finished for good
throw() is the same idea with an error: c.throw(new Error("Stop!")) crashes the generator with Uncaught Error: Stop! and finishes it. A generator can catch that error with try...catch, which you will meet in JS Errors.
function* declarations and expressions, and on object methods (*[Symbol.iterator]()). There is no arrow version: const f = *() => {} is SyntaxError: Unexpected token '*'.
Before async and await existed, programmers used generators to write code that waits for slow tasks (like loading data) in a top-to-bottom style. Today async/await does that job, so you will rarely need generators for it.
yeald 3;. That typo is not a keyword, so the whole script fails to load with SyntaxError: Unexpected number. It should be yield 3;.
What does this log?
function* abc() {
yield "a";
yield "b";
return "c";
}
console.log([...abc()]);
["a", "b"]. The returned "c" comes with done: true, and spread stops there without using it.What happens when this line runs: const g = countToThree();
g is a paused generator objectnewnext().Which is a valid way to get the first 5 values of an infinite generator nums()?
[...nums()].slice(0, 5)nums().toArray().slice(0, 5)nums().take(5).toArray()Array.from(nums()).slice(0, 5)take(5) is lazy, so only 5 values are ever made. The other three all try to collect the endless sequence first and freeze the page.console.log lines between the yields and step through with next()evens(), an infinite generator of even numbers, and take the first 8*[Symbol.iterator]() and loop over it