Generators and Iterators in JavaScript
Iterators
An iterator is an object that provides a next()
method which returns the next item in a sequence. The next()
method returns an object with two properties: value
and done
.
Creating an Iterator
function createIterator(array) {
let index = 0;
return {
next: function() {
return index < array.length
? { value: array[index++], done: false }
: { value: undefined, done: true };
}
};
}
const iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
Iterable Protocol
An object is iterable if it implements the @@iterator
method, which is available via the Symbol.iterator key.
const customIterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
return index < this.data.length
? { value: this.data[index++], done: false }
: { value: undefined, done: true };
}
};
}
};
// Use with for...of loop
for (const item of customIterable) {
console.log(item); // 1, 2, 3
}
// Use with spread operator
const array = [...customIterable]; // [1, 2, 3]
// Use with destructuring
const [a, b, c] = customIterable; // a=1, b=2, c=3
Generators
Generators are special functions that can be paused and resumed, yielding multiple values. They provide an easier way to create iterators.
Basic Generator
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const generator = simpleGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
Generator with Parameters
function* countUp(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
const counter = countUp(5, 8);
console.log(counter.next().value); // 5
console.log(counter.next().value); // 6
console.log(counter.next().value); // 7
console.log(counter.next().value); // 8
console.log(counter.next().done); // true
Passing Values to Generators
function* twoWayGenerator() {
const a = yield 'First yield';
console.log('Received:', a);
const b = yield 'Second yield';
console.log('Received:', b);
return 'Generator done';
}
const gen = twoWayGenerator();
console.log(gen.next().value); // 'First yield'
console.log(gen.next('Response 1').value); // Logs 'Received: Response 1', returns 'Second yield'
console.log(gen.next('Response 2').value); // Logs 'Received: Response 2', returns 'Generator done'
Iterating Over Generators
function* colors() {
yield 'red';
yield 'green';
yield 'blue';
}
// Using for...of loop
for (const color of colors()) {
console.log(color); // 'red', 'green', 'blue'
}
// Using spread operator
const colorArray = [...colors()]; // ['red', 'green', 'blue']
// Using destructuring
const [firstColor, secondColor, thirdColor] = colors();
Generator Delegation
Generators can delegate to other generators using the yield*
expression.
function* generateNumbers() {
yield 1;
yield 2;
}
function* generateLetters() {
yield 'a';
yield 'b';
}
function* generateAll() {
yield* generateNumbers();
yield* generateLetters();
yield 'Done!';
}
const gen = generateAll();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 'a'
console.log(gen.next().value); // 'b'
console.log(gen.next().value); // 'Done!'
Infinite Generators
Generators can represent infinite sequences.
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const numbers = infiniteSequence();
console.log(numbers.next().value); // 0
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
// This could go on forever
Error Handling in Generators
function* errorGenerator() {
try {
yield 1;
yield 2;
yield 3;
} catch (error) {
console.log('Error caught:', error.message);
yield 'Error handled';
}
}
const gen = errorGenerator();
console.log(gen.next().value); // 1
console.log(gen.throw(new Error('Something went wrong')).value); // Logs 'Error caught: Something went wrong', returns 'Error handled'
Practical Use Cases
1. Implementing Iterables
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const range = new Range(1, 5);
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
2. Lazy Evaluation
function* lazyFilter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* lazyMap(iterable, mapper) {
for (const item of iterable) {
yield mapper(item);
}
}
// Create a range from 1 to 10
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
// Compose operations without computing intermediate results
const result = lazyMap(
lazyFilter(range(1, 10), n => n % 2 === 0),
n => n * n
);
for (const value of result) {
console.log(value); // 4, 16, 36, 64, 100
}
3. Asynchronous Iteration
async function* fetchPages(urls) {
for (const url of urls) {
const response = await fetch(url);
const data = await response.json();
yield data;
}
}
// Usage with for-await-of
async function processPages() {
const urls = [
'https://api.example.com/page/1',
'https://api.example.com/page/2',
'https://api.example.com/page/3'
];
for await (const page of fetchPages(urls)) {
console.log(page);
}
}
4. State Machines
function* trafficLight() {
while (true) {
yield 'red';
yield 'green';
yield 'yellow';
}
}
const light = trafficLight();
console.log(light.next().value); // 'red'
console.log(light.next().value); // 'green'
console.log(light.next().value); // 'yellow'
console.log(light.next().value); // 'red' (cycles back)
5. Pagination
function* paginate(array, pageSize) {
for (let i = 0; i < array.length; i += pageSize) {
yield array.slice(i, i + pageSize);
}
}
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const pages = paginate(items, 3);
console.log(pages.next().value); // [1, 2, 3]
console.log(pages.next().value); // [4, 5, 6]
console.log(pages.next().value); // [7, 8, 9]
console.log(pages.next().value); // [10]
Iterator and Generator Methods
Generator Methods
function* generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
// next() - Get the next value
console.log(gen.next()); // { value: 1, done: false }
// return() - Return a value and finish the generator
console.log(gen.return(10)); // { value: 10, done: true }
// throw() - Throw an error into the generator
const gen2 = generator();
gen2.next(); // Get the first value
try {
gen2.throw(new Error('Generator error'));
} catch (e) {
console.log('Caught:', e.message); // 'Caught: Generator error'
}
Built-in Iterables
// String is iterable
for (const char of 'hello') {
console.log(char); // 'h', 'e', 'l', 'l', 'o'
}
// Array is iterable
for (const item of [1, 2, 3]) {
console.log(item); // 1, 2, 3
}
// Map is iterable
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
console.log(key, value); // 'a' 1, 'b' 2
}
// Set is iterable
const set = new Set([1, 2, 3]);
for (const item of set) {
console.log(item); // 1, 2, 3
}
Best Practices
- Use generators for complex iteration logic to make code more readable
- Implement the iterable protocol for custom data structures
- Consider lazy evaluation for performance optimization
- Use generator delegation to compose generators
- Handle errors properly within generators
- Be careful with infinite generators to avoid memory issues
Interview Tips
- Explain the difference between iterators and generators
- Describe how the iterable protocol works in JavaScript
- Explain the purpose of the
yield
keyword in generators - Demonstrate how to pass values back into generators
- Discuss practical use cases for generators and iterators
- Explain how generators can be used for asynchronous programming
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.