An iterator hands out values one at a time: each call to its next() method returns an object like { value: 1, done: false }, until one says done: true. for...of, spread, and destructuring all read values by calling next(). Often the code reading stops before the end, such as a loop that breaks once it finds what it wanted. Closing the iterator means telling it that no more values will be asked for, so it can clean up: stop a timer, close a connection, release a lock. The iteration protocol, the shape every iterator follows, has an optional method for that: return().
It's like telling a waiter you've had enough before the last course: the kitchen stops cooking for you and clears up, instead of keeping your table laid all night.
function* lines() {
console.log("open");
try {
yield "first";
yield "second";
yield "third";
} finally {
console.log("close"); // runs however the generator ends
}
}
for (const line of lines()) {
console.log(line);
if (line === "second") break;
}
// logs open, first, second, close: break called return(), which ran the finally block
const [first] = lines();
// logs open, close: destructuring read one value, then closed the generator
const manual = lines();
manual.next(); // logs open; { value: "first", done: false }
// nothing closes manual for you: it stays paused at its first yield
manual.return("stopped"); // logs close; { value: "stopped", done: true }
manual.next(); // { value: undefined, done: true }
| When the reading stops | Is return() called? |
|---|---|
for...of ends early: break, return, or an exception thrown in the loop body | Yes |
for await...of ends early in the same ways | Yes, on the async iterator, and the loop waits for the promise it returns |
Destructuring has what it needs before the iterator says done, as const [a, b] = iterator does after two values | Yes |
The iterator says done: true, as at the end of every [...iterator] | No: it has already finished |
The iterator's own next() throws | No: the iterator failed, so it's treated as finished |
Your code calls next() by hand and stops calling it | No, unless you call return() yourself |
On a generator, return() resumes it at the yield where it's paused, as if that yield were a return statement. The generator ends there, but its finally blocks run on the way out, so cleanup written in a finally happens however the generator stops. A generator that hasn't started, because next() was never called, finishes at once: its body never runs, so it has nothing to clean up. Calling return() on a generator that has already finished changes nothing.
return() is optional. Array, string, Map, and Set iterators don't have one, since they hold nothing that needs releasing: [1, 2][Symbol.iterator]().return is undefined. So code that closes an iterator calls return() only when it exists. Generators, sync and async, always have one, and an iterator you write by hand can have one too.
You rely on closing whenever a generator holds something open: a loop over it can break early, and the generator still tidies up. Code that reads from an iterator and stops before the end is expected to do what for...of does, and close it.
Lazy evaluation means working out a value only when something asks for it. The opposite, eager evaluation, does all the work up front. Array methods are eager: numbers.map(double) calls double on every element at once, and builds the whole new array before the next line runs. A generator is lazy, or pull-based: the code that reads it pulls each value when it wants one. Until then, the generator does nothing; asked for a value, it works out that one value and pauses again.
Think of a café that makes each coffee when it's ordered, next to one that makes a hundred at opening time. The first never wastes a cup and can keep serving all day. The second has everything ready, but pours away whatever nobody orders.
// Eager: squares every number before looking for one over 10
const numbers = Array.from({ length: 1_000_000 }, (_, i) => i + 1);
numbers.map((n) => n * n).find((n) => n > 10); // 16, after 1,000,000 squarings
// Lazy: squares a number only when the loop asks for the next one
function* squares() {
for (let n = 1; ; n++) {
console.log("squaring", n);
yield n * n;
}
}
const sequence = squares(); // logs nothing: no square has been asked for yet
for (const square of sequence) {
if (square > 10) break;
}
// logs squaring 1, squaring 2, squaring 3, squaring 4, then stops at 16
squares never ends: its loop has no condition. As an array, it could never be built. As a generator, it's fine, because the loop reading it decides how many values to take.
| Question | Eager: an array | Lazy: a generator |
|---|---|---|
| When is each value worked out? | All of them, up front | Each one when it's asked for |
| Can the sequence be endless? | No: the array would never be finished | Yes: the reader takes as many values as it needs |
| What if the reader stops early? | The rest were worked out for nothing | Nothing more is worked out |
| How much memory? | Every value, and each step in a chain like map(...).filter(...) builds a whole new array | The value being worked on, however many there are |
| Is the length known up front? | Yes: length | No: you find out by reading to the end |
| Can you read it twice? | As often as you like | No: a generator object is finished after one read |
Those last rows are the costs. A lazy sequence doesn't know how long it is until it has been read to the end, which for an endless one is never. A generator object can be read only once: spread it to the end, and spreading it again gives [], so to read the values again you call the generator function for a new one. And work happens later than the code that sets it up: calling squares() ran none of its body, so its logging, and any error it throws, comes only when a value is pulled, maybe far from where the sequence was created.
You meet laziness in generators, in the iterator helpers built into current browsers and Node, such as map, filter, and take on iterators, which pass values along one at a time, and in streams, which handle data a chunk at a time instead of all at once. The same idea, putting work off until it's needed, is behind loading="lazy" on images and React.lazy for loading a component's code.
Write a generator function fibonacci(). It yields the Fibonacci numbers forever: 0, 1, and then each number is the sum of the two before it. Every call returns a new generator that starts again from 0.
Write a generator function take(iterable, n) too. It yields the first n values of iterable, then stops, or stops sooner if iterable runs out. iterable can be any iterable: an array, a string, a Set, or an endless generator like fibonacci(). When n is 0, it yields nothing.
take is lazy. Calling it pulls nothing from iterable; each value is pulled when it's asked for, and it never pulls more than n values.
When take stops before iterable runs out, it closes the iterable's iterator (calls its return() method), so a generator's finally blocks run.
const fib = fibonacci();
fib.next(); // { value: 0, done: false }
fib.next(); // { value: 1, done: false }
fib.next(); // { value: 1, done: false }
fib.next(); // { value: 2, done: false }
[...take(fibonacci(), 8)]; // [0, 1, 1, 2, 3, 5, 8, 13]
[...take("hello", 3)]; // ["h", "e", "l"]
[...take([1, 2], 5)]; // [1, 2]
[...take([1, 2], 0)]; // []
const fib = fibonacci();
const first = [];
for (let i = 0; i < 10; i++) first.push(fib.next().value);
expect(first).toEqual([0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
const fib = fibonacci();
let result;
for (let i = 0; i < 50; i++) result = fib.next();
expect(result).toEqual({ value: 7778742049, done: false });
const first = fibonacci();
first.next();
first.next();
first.next();
const second = fibonacci();
expect(second.next().value).toBe(0);
expect(first.next().value).toBe(2);
expect([...take(fibonacci(), 8)]).toEqual([0, 1, 1, 2, 3, 5, 8, 13]);
expect([...take("hello", 3)]).toEqual(["h", "e", "l"]);
expect([...take(new Set([1, 2, 3]), 2)]).toEqual([1, 2]);
expect([...take([1, 2], 5)]).toEqual([1, 2]);
let pulls = 0;
const upToTen = {
[Symbol.iterator]() {
return { next: () => (pulls < 10 ? { value: ++pulls, done: false } : { value: undefined, done: true }) };
},
};
const firstThree = take(upToTen, 3);
expect(pulls).toBe(0);
expect([...firstThree]).toEqual([1, 2, 3]);
expect(pulls).toBe(3);
let pulls = 0;
const upToTen = {
[Symbol.iterator]() {
return { next: () => (pulls < 10 ? { value: ++pulls, done: false } : { value: undefined, done: true }) };
},
};
expect([...take(upToTen, 0)]).toEqual([]);
expect(pulls).toBe(0);
let closed = false;
function* letters() {
try {
yield "a";
yield "b";
yield "c";
} finally {
closed = true;
}
}
expect([...take(letters(), 2)]).toEqual(["a", "b"]);
expect(closed).toBe(true);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Fibonacci generator with take. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.