JavaScript Generators Tutorial - Section 1: Introduction
The iterator protocol defines a standard way to produce a sequence of values. An object is an iterator when it implements a next() method that returns an object with value and done properties.
// Iterator must have a next() method
const iterator = {
next() {
return {
value: any, // The current value
done: boolean // true when iteration is complete
};
}
};
function createRangeIterator(start, end) {
let current = start;
return {
next() {
if (current <= end) {
return {
value: current++,
done: false
};
} else {
return {
done: true
};
}
}
};
}
const iterator = createRangeIterator(1, 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()); // { done: true }
An object is iterable if it implements the @@iterator method (accessible via Symbol.iterator), which returns an iterator.
const iterable = {
[Symbol.iterator]() {
return iterator; // Returns an iterator
}
};
Many JavaScript objects are iterable by default:
// Arrays
const arr = [1, 2, 3];
for (const value of arr) {
console.log(value); // 1, 2, 3
}
// Strings
const str = 'hello';
for (const char of str) {
console.log(char); // h, e, l, l, o
}
// Maps
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
console.log(key, value); // a 1, b 2
}
// Sets
const set = new Set([1, 2, 3]);
for (const value of set) {
console.log(value); // 1, 2, 3
}
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return {
value: current++,
done: false
};
}
return { done: true };
}
};
}
}
const range = new Range(1, 5);
// Works with for...of
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// Works with spread operator
console.log([...range]); // [1, 2, 3, 4, 5]
// Works with Array.from()
console.log(Array.from(range)); // [1, 2, 3, 4, 5]
Generators automatically implement the iterator protocol:
// Without generator (manual iterator)
class RangeManual {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
}
// With generator (much simpler!)
class RangeGenerator {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const range1 = new RangeManual(1, 3);
const range2 = new RangeGenerator(1, 3);
console.log([...range1]); // [1, 2, 3]
console.log([...range2]); // [1, 2, 3]
const iterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
};
for (const value of iterable) {
console.log(value); // 1, 2, 3
}
function* numbers() {
yield 1;
yield 2;
yield 3;
}
const arr = [...numbers()];
console.log(arr); // [1, 2, 3]
const [a, b, c] = numbers();
console.log(a, b, c); // 1 2 3
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
const arr = Array.from(range(1, 5));
console.log(arr); // [1, 2, 3, 4, 5]
// Map, filter, reduce work on arrays created from iterables
const doubled = Array.from(range(1, 5)).map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
class LinkedList {
constructor() {
this.head = null;
}
add(value) {
const node = { value, next: null };
if (!this.head) {
this.head = node;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = node;
}
}
*[Symbol.iterator]() {
let current = this.head;
while (current) {
yield current.value;
current = current.next;
}
}
}
const list = new LinkedList();
list.add(1);
list.add(2);
list.add(3);
// Now iterable!
for (const value of list) {
console.log(value); // 1, 2, 3
}
console.log([...list]); // [1, 2, 3]
Iterators can optionally implement return() and throw() methods:
function* generatorWithCleanup() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log('Cleanup!');
}
}
const gen = generatorWithCleanup();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.return('early exit'));
// Logs: Cleanup!
// Returns: { value: 'early exit', done: true }
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const seq = infiniteSequence();
console.log(seq.next().value); // 0
console.log(seq.next().value); // 1
console.log(seq.next().value); // 2
// Take first 5 values
function* take(iterable, n) {
let count = 0;
for (const value of iterable) {
if (count++ >= n) break;
yield value;
}
}
console.log([...take(infiniteSequence(), 5)]); // [0, 1, 2, 3, 4]
next() methodSymbol.iteratorfor...of, spread, etc.Now let's explore the basic generator syntax in detail!