Iterators, Generators, and Iteration Protocols
1. Iterator Interface and Iterable Protocol
Iterator Protocol
| Requirement | Description | Returns |
|---|---|---|
| next() method | Returns object with value and done properties | {value: any, done: boolean} |
| value | Current iteration value | Any type |
| done | true if iterator finished, false otherwise | Boolean |
Iterable Protocol
| Requirement | Description | Symbol |
|---|---|---|
| [Symbol.iterator] method | Returns iterator object with next() method | Symbol.iterator |
| Return value | Object implementing iterator protocol | Iterator with next() |
Built-in Iterables
| Type | Iterable | Iteration Order |
|---|---|---|
| Array | ✓ Yes | Index order (0, 1, 2, ...) |
| String | ✓ Yes | Character order |
| Map | ✓ Yes | Insertion order [key, value] pairs |
| Set | ✓ Yes | Insertion order values |
| TypedArray | ✓ Yes | Index order |
| arguments | ✓ Yes | Index order |
| NodeList | ✓ Yes | Index order |
| Object | ✗ No (use Object.keys/values/entries) | N/A |
Consuming Iterables
| Construct | Syntax | Description |
|---|---|---|
| for...of loop | for (const x of iterable) |
Iterate over iterable values |
| Spread operator | [...iterable] |
Convert iterable to array |
| Array.from() | Array.from(iterable) |
Create array from iterable |
| Destructuring | const [a, b] = iterable |
Extract values from iterable |
| yield* | yield* iterable |
Delegate to iterable in generator |
| Promise.all() | Promise.all(iterable) |
Wait for all promises in iterable |
| Map/Set constructor | new Map(iterable) |
Create Map/Set from iterable |
Example: Understanding iterator protocol
// Array is iterable
const arr = [1, 2, 3];
// Get iterator from iterable
const iterator = arr[Symbol.iterator]();
// Call next() manually
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}
console.log(iterator.next()); // {value: undefined, done: true}
// for...of uses iterator protocol internally
for (const value of arr) {
console.log(value); // 1, 2, 3
}
// String is iterable
const str = 'hello';
const strIterator = str[Symbol.iterator]();
console.log(strIterator.next()); // {value: 'h', done: false}
console.log(strIterator.next()); // {value: 'e', done: false}
for (const char of str) {
console.log(char); // 'h', 'e', 'l', 'l', 'o'
}
// Map is iterable (yields [key, value] pairs)
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 value of set) {
console.log(value); // 1, 2, 3
}
Example: Creating simple iterator
// Simple iterator (not iterable)
function createRangeIterator(start, end) {
let current = start;
return {
next() {
if (current <= end) {
return {
value: current++,
done: false
};
}
return {
value: undefined,
done: true
};
}
};
}
const rangeIter = createRangeIterator(1, 5);
console.log(rangeIter.next()); // {value: 1, done: false}
console.log(rangeIter.next()); // {value: 2, done: false}
console.log(rangeIter.next()); // {value: 3, done: false}
console.log(rangeIter.next()); // {value: 4, done: false}
console.log(rangeIter.next()); // {value: 5, done: false}
console.log(rangeIter.next()); // {value: undefined, done: true}
// Cannot use with for...of (not iterable, just iterator)
// for (const n of rangeIter) {} // TypeError
Example: Creating iterable object
// Iterable object (has Symbol.iterator method)
const range = {
start: 1,
end: 5,
// Implement iterable protocol
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
// Return iterator
return {
next() {
if (current <= end) {
return {
value: current++,
done: false
};
}
return {
value: undefined,
done: true
};
}
};
}
};
// Can use with for...of
for (const n of range) {
console.log(n); // 1, 2, 3, 4, 5
}
// Can use spread operator
const arr2 = [...range];
console.log(arr2); // [1, 2, 3, 4, 5]
// Can destructure
const [first, second, ...rest] = range;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
// Each call to Symbol.iterator creates new iterator
const iter1 = range[Symbol.iterator]();
const iter2 = range[Symbol.iterator]();
console.log(iter1.next()); // {value: 1, done: false}
console.log(iter2.next()); // {value: 1, done: false} (independent)
Example: Iterable with state
// Iterable collection
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
append(value) {
const node = {value, next: null};
if (!this.head) {
this.head = node;
this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this.length++;
}
// Make iterable
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
const value = current.value;
current = current.next;
return {value, done: false};
}
return {value: undefined, done: true};
}
};
}
}
const list = new LinkedList();
list.append(10);
list.append(20);
list.append(30);
// Use with for...of
for (const value of list) {
console.log(value); // 10, 20, 30
}
// Convert to array
const listArray = [...list];
console.log(listArray); // [10, 20, 30]
// Use Array methods via spread
const doubled = [...list].map(x => x * 2);
console.log(doubled); // [20, 40, 60]
Key Points: Iterator protocol: object with next() returning
{value, done}. Iterable protocol: object with [Symbol.iterator] method returning
iterator. Built-in iterables: Array, String, Map, Set, TypedArray. Iterables work with for...of, spread,
destructuring. Each call to Symbol.iterator creates fresh iterator.
2. Generator Functions and yield Expressions
Generator Function Syntax
| Type | Syntax | Example |
|---|---|---|
| Function Declaration | function* name() {} |
function* gen() { yield 1; } |
| Function Expression | const f = function*() {} |
const g = function*() { yield 1; } |
| Method | *method() {} |
obj = {*gen() { yield 1; }} |
| Class Method | *method() {} |
class C {*gen() { yield 1; }} |
| Arrow Function | ❌ Not supported | Cannot create generator arrows |
yield Expression Types
| Expression | Syntax | Description | Returns |
|---|---|---|---|
| yield | yield value |
Pause and yield value | Value from next() or undefined |
| yield* | yield* iterable |
Delegate to iterable/generator | Final return value of iterable |
| yield (no value) | yield |
Pause, yield undefined | Value from next() |
| yield (assignment) | const x = yield |
Receive value from next(value) | Value passed to next() |
Generator Object Properties
| Feature | Description | Behavior |
|---|---|---|
| Is Iterable | Has [Symbol.iterator]() method | Returns itself |
| Is Iterator | Has next() method | Returns {value, done} |
| Lazy | Executes on demand | Code runs only when next() called |
| Pausable | Execution pauses at yield | Resumes on next next() call |
| State | Maintains local state between yields | Variables persist across pauses |
Example: Basic generator functions
// Simple generator
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = simpleGenerator();
// Generator is both iterator and iterable
console.log(typeof gen.next); // "function" (iterator)
console.log(typeof gen[Symbol.iterator]); // "function" (iterable)
// Use as iterator
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.next()); // {value: 2, done: false}
console.log(gen.next()); // {value: 3, done: false}
console.log(gen.next()); // {value: undefined, done: true}
// Use as iterable
function* gen2() {
yield 1;
yield 2;
yield 3;
}
for (const value of gen2()) {
console.log(value); // 1, 2, 3
}
// Spread operator
const values = [...gen2()];
console.log(values); // [1, 2, 3]
// Generator with logic
function* fibonacci(n) {
let [a, b] = [0, 1];
let count = 0;
while (count < n) {
yield a;
[a, b] = [b, a + b];
count++;
}
}
console.log([...fibonacci(10)]);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Example: yield expressions and two-way communication
// Generator receiving values
function* twoWay() {
console.log('Start');
const a = yield 'First';
console.log('Received:', a);
const b = yield 'Second';
console.log('Received:', b);
return 'Done';
}
const gen3 = twoWay();
// First next() starts generator
console.log(gen3.next()); // Logs: "Start"
// Returns: {value: 'First', done: false}
// Send value 10
console.log(gen3.next(10)); // Logs: "Received: 10"
// Returns: {value: 'Second', done: false}
// Send value 20
console.log(gen3.next(20)); // Logs: "Received: 20"
// Returns: {value: 'Done', done: true}
// Practical example: ID generator
function* idGenerator(prefix = 'ID') {
let id = 1;
while (true) {
const reset = yield `${prefix}-${id}`;
if (reset) {
id = 1;
} else {
id++;
}
}
}
const ids = idGenerator('USER');
console.log(ids.next().value); // "USER-1"
console.log(ids.next().value); // "USER-2"
console.log(ids.next().value); // "USER-3"
console.log(ids.next(true).value); // "USER-1" (reset)
console.log(ids.next().value); // "USER-2"
Example: yield* delegation
// Delegate to another generator
function* gen1() {
yield 1;
yield 2;
}
function* gen2() {
yield 'a';
yield* gen1(); // Delegate to gen1
yield 'b';
}
console.log([...gen2()]); // ['a', 1, 2, 'b']
// Delegate to any iterable
function* gen3() {
yield* [1, 2, 3];
yield* 'hello';
yield* new Set([4, 5, 6]);
}
console.log([...gen3()]);
// [1, 2, 3, 'h', 'e', 'l', 'l', 'o', 4, 5, 6]
// Tree traversal
const tree = {
value: 1,
children: [
{
value: 2,
children: [
{value: 4, children: []},
{value: 5, children: []}
]
},
{
value: 3,
children: [
{value: 6, children: []}
]
}
]
};
function* traverse(node) {
yield node.value;
for (const child of node.children) {
yield* traverse(child); // Recursive delegation
}
}
console.log([...traverse(tree)]); // [1, 2, 4, 5, 3, 6]
// Flatten nested arrays
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item);
} else {
yield item;
}
}
}
const nested = [1, [2, [3, 4], 5], 6];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5, 6]
// yield* returns the final value
function* innerGen() {
yield 1;
yield 2;
return 'finished';
}
function* outerGen() {
const result = yield* innerGen();
console.log('Inner returned:', result); // "finished"
yield 3;
}
console.log([...outerGen()]);
// Logs: "Inner returned: finished"
// Returns: [1, 2, 3] (return value not included in iteration)
Example: Infinite sequences and lazy evaluation
// Infinite sequence
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
const nums = naturals();
console.log(nums.next().value); // 1
console.log(nums.next().value); // 2
console.log(nums.next().value); // 3
// Take first N
function* take(n, iterable) {
let count = 0;
for (const value of iterable) {
if (count++ >= n) break;
yield value;
}
}
console.log([...take(5, naturals())]); // [1, 2, 3, 4, 5]
// Filter
function* filter(predicate, iterable) {
for (const value of iterable) {
if (predicate(value)) {
yield value;
}
}
}
function* evens() {
yield* filter(n => n % 2 === 0, naturals());
}
console.log([...take(5, evens())]); // [2, 4, 6, 8, 10]
// Map
function* map(fn, iterable) {
for (const value of iterable) {
yield fn(value);
}
}
function* squares() {
yield* map(n => n ** 2, naturals());
}
console.log([...take(5, squares())]); // [1, 4, 9, 16, 25]
// Compose transformations (lazy!)
const result = take(5,
map(n => n ** 2,
filter(n => n % 2 === 0, naturals())
)
);
console.log([...result]); // [4, 16, 36, 64, 100]
// Random number generator
function* randomInRange(min, max) {
while (true) {
yield Math.floor(Math.random() * (max - min + 1)) + min;
}
}
const dice = randomInRange(1, 6);
console.log([...take(10, dice)]); // 10 random numbers 1-6
Key Points: Generators are both iterator and iterable. Execution
pauses at yield, resumes at next next().
yield* delegates to another iterable. Generators enable
lazy evaluation (compute on demand). Two-way communication: yield sends value
out,
next(value) sends value in. Perfect for infinite sequences and streaming data.
3. Generator Methods and Return Values
Generator Methods
| Method | Syntax | Description | Effect |
|---|---|---|---|
| next() | gen.next(value) |
Resume execution, send value to yield | Returns {value, done} |
| return() | gen.return(value) |
Terminate generator, set return value | Returns {value, done: true} |
| throw() | gen.throw(error) |
Throw error at yield point | Returns {value, done} or throws |
Generator Return Behavior
| Scenario | Result | done | value |
|---|---|---|---|
| yield expression | Normal iteration | false | Yielded value |
| return statement | Generator completes | true | Returned value |
| Implicit return (end) | Generator completes | true | undefined |
| gen.return(value) | Forced completion | true | Provided value |
| gen.throw(error) | Error thrown at yield | Depends on handling | Next yield or throws |
finally Block Behavior
| Method | finally Executes | Notes |
|---|---|---|
| return() | ✓ Yes | Cleanup code runs before completion |
| throw() | ✓ Yes | Runs before exception propagates |
| Normal completion | ✓ Yes | Runs when generator ends normally |
Example: Generator return() method
// Early termination with return()
function* gen4() {
try {
yield 1;
yield 2;
yield 3;
yield 4;
} finally {
console.log('Cleanup');
}
}
const g4 = gen4();
console.log(g4.next()); // {value: 1, done: false}
console.log(g4.next()); // {value: 2, done: false}
console.log(g4.return(99)); // Logs: "Cleanup"
// Returns: {value: 99, done: true}
console.log(g4.next()); // {value: undefined, done: true}
// return() without value
function* gen5() {
yield 1;
yield 2;
yield 3;
}
const g5 = gen5();
console.log(g5.next()); // {value: 1, done: false}
console.log(g5.return()); // {value: undefined, done: true}
console.log(g5.next()); // {value: undefined, done: true}
// Using return in for...of
function* gen6() {
yield 1;
yield 2;
return 3; // Return value not included in iteration
yield 4; // Never reached
}
const values = [...gen6()];
console.log(values); // [1, 2] (return value excluded)
// But visible with manual next()
const g6 = gen6();
console.log(g6.next()); // {value: 1, done: false}
console.log(g6.next()); // {value: 2, done: false}
console.log(g6.next()); // {value: 3, done: true} (return value visible)
Example: Generator throw() method
// Error handling with throw()
function* gen7() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.log('Caught:', e.message);
yield 'error handled';
}
yield 4;
}
const g7 = gen7();
console.log(g7.next()); // {value: 1, done: false}
console.log(g7.throw(new Error('Oops'))); // Logs: "Caught: Oops"
// Returns: {value: 'error handled', done: false}
console.log(g7.next()); // {value: 4, done: false}
console.log(g7.next()); // {value: undefined, done: true}
// Uncaught error
function* gen8() {
yield 1;
yield 2; // No try-catch
yield 3;
}
const g8 = gen8();
console.log(g8.next()); // {value: 1, done: false}
try {
g8.throw(new Error('Uncaught'));
} catch (e) {
console.log('Exception propagated:', e.message);
// "Exception propagated: Uncaught"
}
console.log(g8.next()); // {value: undefined, done: true} (generator closed)
// finally with throw
function* gen9() {
try {
yield 1;
yield 2;
} finally {
console.log('Cleanup after error');
}
}
const g9 = gen9();
console.log(g9.next()); // {value: 1, done: false}
try {
g9.throw(new Error('Error'));
} catch (e) {
console.log('Caught outside');
// Logs: "Cleanup after error" (finally runs)
// Logs: "Caught outside"
}
Example: Cleanup with finally
// Resource management
function* databaseQuery() {
console.log('Opening connection');
try {
yield 'row 1';
yield 'row 2';
yield 'row 3';
} finally {
console.log('Closing connection');
}
}
// Normal completion
console.log('--- Normal completion ---');
for (const row of databaseQuery()) {
console.log(row);
}
// Logs:
// "Opening connection"
// "row 1"
// "row 2"
// "row 3"
// "Closing connection"
// Early termination
console.log('--- Early termination ---');
const query = databaseQuery();
console.log(query.next().value); // "Opening connection", "row 1"
console.log(query.next().value); // "row 2"
query.return(); // "Closing connection"
// Break in for...of
console.log('--- Break in loop ---');
for (const row of databaseQuery()) {
console.log(row);
if (row === 'row 2') break; // Triggers cleanup
}
// Logs:
// "Opening connection"
// "row 1"
// "row 2"
// "Closing connection"
// Error handling
console.log('--- Error ---');
function* withCleanup() {
console.log('Setup');
try {
yield 1;
throw new Error('Internal error');
yield 2; // Never reached
} catch (e) {
console.log('Error in generator:', e.message);
} finally {
console.log('Cleanup');
}
}
for (const value of withCleanup()) {
console.log(value);
}
// Logs:
// "Setup"
// "1"
// "Error in generator: Internal error"
// "Cleanup"
Example: Return values and yield*
// Return value from generator
function* inner() {
yield 1;
yield 2;
return 'inner done';
}
function* outer1() {
const result = yield* inner();
console.log('Inner returned:', result);
yield 3;
}
console.log([...outer1()]);
// Logs: "Inner returned: inner done"
// Returns: [1, 2, 3]
// Return value not included in iteration
function* gen10() {
yield 1;
yield 2;
return 42;
}
console.log([...gen10()]); // [1, 2] (42 not included)
// But captured by yield*
function* outer2() {
const returnValue = yield* gen10();
console.log('Got return value:', returnValue);
yield returnValue;
}
console.log([...outer2()]);
// Logs: "Got return value: 42"
// Returns: [1, 2, 42]
// Chaining generators
function* pipeline() {
const r1 = yield* stage1();
console.log('Stage 1 result:', r1);
const r2 = yield* stage2(r1);
console.log('Stage 2 result:', r2);
return r2;
}
function* stage1() {
yield 'processing...';
return 'stage1-complete';
}
function* stage2(input) {
yield `using ${input}`;
return 'stage2-complete';
}
console.log([...pipeline()]);
// Logs: "Stage 1 result: stage1-complete"
// Logs: "Stage 2 result: stage2-complete"
// Returns: ["processing...", "using stage1-complete"]
// Final result
const gen11 = pipeline();
let result;
while (true) {
result = gen11.next();
if (result.done) {
console.log('Final return:', result.value); // "stage2-complete"
break;
}
}
Important:
return() terminates generator and runs finally blocks.
throw()
throws error at yield point (must be caught or generator closes). Return values from generators not included in iteration but captured by yield*. Use finally for
cleanup. Early break in for...of triggers cleanup via return().
4. Async Generators and for-await-of Loops
Async Generator Syntax
| Type | Syntax | Returns |
|---|---|---|
| Function Declaration | async function* name() {} |
AsyncGenerator object |
| Function Expression | const f = async function*() {} |
AsyncGenerator object |
| Method | async *method() {} |
AsyncGenerator object |
| Class Method | async *method() {} |
AsyncGenerator object |
AsyncGenerator Methods
| Method | Syntax | Description | Returns |
|---|---|---|---|
| next() | await gen.next(value) |
Resume, send value | Promise<{value, done}> |
| return() | await gen.return(value) |
Terminate generator | Promise<{value, done: true}> |
| throw() | await gen.throw(error) |
Throw error at yield | Promise<{value, done}> |
for-await-of Loop
| Feature | Syntax | Works With |
|---|---|---|
| Basic | for await (const x of asyncIterable) |
Async iterables |
| Await promises | Automatically awaits each value | AsyncGenerator, async iterables |
| Context | Must be in async function | Any async function or top-level |
| Break/Continue | Supported | Same as regular for...of |
Async Iterable Protocol
| Requirement | Description | Returns |
|---|---|---|
| [Symbol.asyncIterator] | Method returning async iterator | Object with next() returning Promise |
| next() method | Returns promise of {value, done} | Promise<{value: any, done: boolean}> |
Example: Basic async generators
// Simple async generator
async function* asyncNumbers() {
yield 1;
yield 2;
yield 3;
}
// Consume with for-await-of
async function main1() {
for await (const num of asyncNumbers()) {
console.log(num); // 1, 2, 3
}
}
main1();
// Async generator with delays
async function* delayedSequence() {
yield await Promise.resolve(1);
await new Promise(resolve => setTimeout(resolve, 100));
yield 2;
await new Promise(resolve => setTimeout(resolve, 100));
yield 3;
}
async function main2() {
console.log('Start');
for await (const value of delayedSequence()) {
console.log(value); // 1 (then 100ms), 2 (then 100ms), 3
}
console.log('Done');
}
main2();
// Fetch data in chunks
async function* fetchPages(baseUrl, maxPages) {
for (let page = 1; page <= maxPages; page++) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
yield data;
}
}
async function processAllPages() {
for await (const pageData of fetchPages('/api/items', 5)) {
console.log('Processing page:', pageData);
// Process page data
}
}
Example: Async generator methods
// Manual iteration
async function* asyncGen() {
yield 1;
yield 2;
yield 3;
}
async function manual() {
const gen = asyncGen();
console.log(await gen.next()); // {value: 1, done: false}
console.log(await gen.next()); // {value: 2, done: false}
console.log(await gen.next()); // {value: 3, done: false}
console.log(await gen.next()); // {value: undefined, done: true}
}
manual();
// Early termination with return()
async function* withCleanup() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log('Cleanup');
}
}
async function earlyExit() {
const gen = withCleanup();
console.log(await gen.next()); // {value: 1, done: false}
console.log(await gen.return(99)); // Logs: "Cleanup"
// Returns: {value: 99, done: true}
console.log(await gen.next()); // {value: undefined, done: true}
}
earlyExit();
// Error handling with throw()
async function* withErrorHandling() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.log('Caught:', e.message);
yield 'recovered';
}
}
async function throwError() {
const gen = withErrorHandling();
console.log(await gen.next()); // {value: 1, done: false}
console.log(await gen.throw(new Error('Oops'))); // Logs: "Caught: Oops"
// Returns: {value: 'recovered', done: false}
console.log(await gen.next()); // {value: undefined, done: true}
}
throwError();
Example: Streaming data processing
// Read file in chunks
async function* readFileInChunks(filename, chunkSize = 1024) {
// Simulated file reading (in real code, use fs.createReadStream)
const fileContent = 'Large file content...';
for (let i = 0; i < fileContent.length; i += chunkSize) {
const chunk = fileContent.slice(i, i + chunkSize);
yield chunk;
// Simulate async I/O
await new Promise(resolve => setTimeout(resolve, 10));
}
}
async function processFile() {
for await (const chunk of readFileInChunks('data.txt', 100)) {
console.log('Processing chunk:', chunk.length, 'bytes');
}
}
// Event stream
async function* eventStream(eventEmitter, eventName) {
const queue = [];
let resolveNext;
const handler = (data) => {
if (resolveNext) {
resolveNext(data);
resolveNext = null;
} else {
queue.push(data);
}
};
eventEmitter.on(eventName, handler);
try {
while (true) {
if (queue.length > 0) {
yield queue.shift();
} else {
yield await new Promise(resolve => {
resolveNext = resolve;
});
}
}
} finally {
eventEmitter.off(eventName, handler);
}
}
// Usage with EventEmitter
async function processEvents(emitter) {
for await (const event of eventStream(emitter, 'data')) {
console.log('Event:', event);
if (event.type === 'end') break;
}
}
// WebSocket stream
async function* websocketStream(url) {
const ws = new WebSocket(url);
const queue = [];
let resolveNext;
let done = false;
ws.onmessage = (event) => {
if (resolveNext) {
resolveNext({value: event.data, done: false});
resolveNext = null;
} else {
queue.push(event.data);
}
};
ws.onclose = () => {
done = true;
if (resolveNext) {
resolveNext({done: true});
}
};
try {
while (!done) {
if (queue.length > 0) {
yield queue.shift();
} else {
const result = await new Promise(resolve => {
resolveNext = resolve;
});
if (result.done) break;
yield result.value;
}
}
} finally {
ws.close();
}
}
async function processWebSocketMessages() {
for await (const message of websocketStream('ws://example.com')) {
console.log('Message:', message);
}
}
Example: Combining async generators
// Merge multiple async streams
async function* merge(...asyncIterables) {
const iterators = asyncIterables.map(it => it[Symbol.asyncIterator]());
const promises = new Map(
iterators.map((it, i) => [i, it.next().then(result => ({i, result}))])
);
while (promises.size > 0) {
const {i, result} = await Promise.race(promises.values());
if (result.done) {
promises.delete(i);
} else {
yield result.value;
promises.set(i,
iterators[i].next().then(result => ({i, result}))
);
}
}
}
async function* source1() {
yield 1;
await new Promise(r => setTimeout(r, 100));
yield 2;
}
async function* source2() {
yield 'a';
await new Promise(r => setTimeout(r, 50));
yield 'b';
}
async function testMerge() {
for await (const value of merge(source1(), source2())) {
console.log(value); // Order depends on timing: a, 1, b, 2 or a, b, 1, 2
}
}
// Transform async stream
async function* map(fn, asyncIterable) {
for await (const value of asyncIterable) {
yield fn(value);
}
}
async function* filter(predicate, asyncIterable) {
for await (const value of asyncIterable) {
if (predicate(value)) {
yield value;
}
}
}
async function* numbers() {
for (let i = 1; i <= 10; i++) {
yield i;
await new Promise(r => setTimeout(r, 10));
}
}
async function processNumbers() {
const evens = filter(n => n % 2 === 0, numbers());
const doubled = map(n => n * 2, evens);
for await (const value of doubled) {
console.log(value); // 4, 8, 12, 16, 20
}
}
// Throttle async stream
async function* throttle(asyncIterable, delay) {
for await (const value of asyncIterable) {
yield value;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
async function throttleExample() {
for await (const num of throttle(numbers(), 100)) {
console.log(num); // 1, (100ms), 2, (100ms), 3, ...
}
}
Key Points:
async function* creates async generator. Use
for await...of
to iterate (must be in async context). Methods return promises: await gen.next(). Perfect for
streaming data, event processing, paginated APIs. AsyncGenerator is both async
iterable and async iterator.
5. Custom Iterator Implementation
Iterator Implementation Checklist
| Step | Requirement | Implementation |
|---|---|---|
| 1. Make iterable | Add [Symbol.iterator] method | Returns iterator object |
| 2. Return iterator | Iterator has next() method | Returns {value, done} |
| 3. Track state | Maintain iteration position | Use closure or object properties |
| 4. Signal completion | Set done: true when finished | Return {value: undefined, done: true} |
| 5. Optional: return/throw | Add cleanup support | Implement return() and throw() methods |
Iterator Pattern Variations
| Pattern | When to Use | Example |
|---|---|---|
| Simple Iterator | Single iteration, no cleanup | Range, sequence |
| Reusable Iterator | Multiple iterations from same object | Array, collection |
| Stateful Iterator | Complex state management | Tree traversal, pagination |
| Iterator with Cleanup | Resource management needed | File reader, DB cursor |
| Infinite Iterator | Unbounded sequences | Counter, random generator |
Example: Custom collection iterators
// Simple range iterator
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
const step = this.step;
return {
next() {
if (current <= end) {
const value = current;
current += step;
return {value, done: false};
}
return {value: undefined, done: true};
}
};
}
}
const range = new Range(1, 10, 2);
for (const n of range) {
console.log(n); // 1, 3, 5, 7, 9
}
console.log([...range]); // [1, 3, 5, 7, 9]
// Multiple independent iterations
const iter1 = range[Symbol.iterator]();
const iter2 = range[Symbol.iterator]();
console.log(iter1.next().value); // 1
console.log(iter2.next().value); // 1 (independent)
console.log(iter1.next().value); // 3
console.log(iter2.next().value); // 3
// Linked list iterator
class Node {
constructor(value, next = null) {
this.value = value;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
this.size = 0;
}
append(value) {
const node = new Node(value);
if (!this.head) {
this.head = node;
this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this.size++;
}
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
const value = current.value;
current = current.next;
return {value, done: false};
}
return {value: undefined, done: true};
}
};
}
}
const list = new LinkedList();
list.append(10);
list.append(20);
list.append(30);
for (const value of list) {
console.log(value); // 10, 20, 30
}
// Map and filter on custom iterable
const doubled = [...list].map(x => x * 2);
console.log(doubled); // [20, 40, 60]
Example: Iterators with cleanup
// Iterator with return() method
class FileReader {
constructor(filename) {
this.filename = filename;
}
[Symbol.iterator]() {
let file = this.openFile(this.filename);
let lineNumber = 0;
return {
next() {
const line = this.readLine(file);
if (line !== null) {
return {
value: {lineNumber: ++lineNumber, text: line},
done: false
};
}
this.closeFile(file);
return {value: undefined, done: true};
}.bind(this),
return(value) {
// Cleanup on early termination
console.log('Closing file early');
this.closeFile(file);
return {value, done: true};
}.bind(this)
};
}
openFile(filename) {
console.log('Opening file:', filename);
return {/* file handle */};
}
readLine(file) {
// Simulated
return null;
}
closeFile(file) {
console.log('Closing file');
}
}
const reader = new FileReader('data.txt');
// Early break triggers return()
for (const line of reader) {
console.log(line);
if (line.lineNumber === 5) break; // Calls return()
}
// Logs: "Closing file early"
// Iterator with throw() support
class SafeIterator {
constructor(items) {
this.items = items;
}
[Symbol.iterator]() {
let index = 0;
const items = this.items;
let errorHandled = false;
return {
next() {
if (errorHandled) {
return {value: undefined, done: true};
}
if (index < items.length) {
return {value: items[index++], done: false};
}
return {value: undefined, done: true};
},
throw(error) {
console.log('Error in iterator:', error.message);
errorHandled = true;
return {value: 'error-handled', done: false};
},
return(value) {
console.log('Cleanup');
return {value, done: true};
}
};
}
}
const safe = new SafeIterator([1, 2, 3]);
const safeIter = safe[Symbol.iterator]();
console.log(safeIter.next()); // {value: 1, done: false}
console.log(safeIter.throw(new Error('Oops'))); // Logs: "Error in iterator: Oops"
// Returns: {value: 'error-handled', done: false}
console.log(safeIter.next()); // {value: undefined, done: true}
Example: Advanced iterator patterns
// Bidirectional iterator
class BidirectionalList {
constructor(items) {
this.items = items;
}
[Symbol.iterator]() {
let index = 0;
const items = this.items;
return {
next() {
if (index < items.length) {
return {value: items[index++], done: false};
}
return {value: undefined, done: true};
},
// Custom method for backward iteration
prev() {
if (index > 0) {
return {value: items[--index], done: false};
}
return {value: undefined, done: true};
}
};
}
}
const biList = new BidirectionalList([1, 2, 3, 4, 5]);
const biIter = biList[Symbol.iterator]();
console.log(biIter.next().value); // 1
console.log(biIter.next().value); // 2
console.log(biIter.next().value); // 3
console.log(biIter.prev().value); // 2
console.log(biIter.prev().value); // 1
console.log(biIter.next().value); // 2
// Peekable iterator
class PeekableIterator {
constructor(iterable) {
this.iterator = iterable[Symbol.iterator]();
this.peeked = null;
this.hasPeeked = false;
}
next() {
if (this.hasPeeked) {
this.hasPeeked = false;
return this.peeked;
}
return this.iterator.next();
}
peek() {
if (!this.hasPeeked) {
this.peeked = this.iterator.next();
this.hasPeeked = true;
}
return this.peeked;
}
[Symbol.iterator]() {
return this;
}
}
const peekable = new PeekableIterator([1, 2, 3, 4, 5]);
console.log(peekable.peek().value); // 1 (doesn't advance)
console.log(peekable.peek().value); // 1 (still doesn't advance)
console.log(peekable.next().value); // 1 (now advances)
console.log(peekable.next().value); // 2
console.log(peekable.peek().value); // 3
console.log(peekable.next().value); // 3
// Cycling iterator
class CycleIterator {
constructor(iterable) {
this.iterable = iterable;
this.iterator = null;
}
[Symbol.iterator]() {
return this;
}
next() {
if (!this.iterator) {
this.iterator = this.iterable[Symbol.iterator]();
}
const result = this.iterator.next();
if (result.done) {
// Restart
this.iterator = this.iterable[Symbol.iterator]();
return this.iterator.next();
}
return result;
}
}
const cycle = new CycleIterator([1, 2, 3]);
console.log(cycle.next().value); // 1
console.log(cycle.next().value); // 2
console.log(cycle.next().value); // 3
console.log(cycle.next().value); // 1 (cycles back)
console.log(cycle.next().value); // 2
console.log(cycle.next().value); // 3
console.log(cycle.next().value); // 1 (cycles again)
Key Points: Implement
[Symbol.iterator]() returning object with
next().
Track state with closure or properties. Optional: return() for cleanup, throw() for
error handling. Each [Symbol.iterator]() call should return new independent
iterator. Use generators for simpler implementation.
6. Iterator Helper Methods (take, drop, filter)
Iterator Helper Methods (ES2024+) New
| Method | Syntax | Description | Returns |
|---|---|---|---|
| take() | iter.take(n) |
Take first n elements | Iterator |
| drop() | iter.drop(n) |
Skip first n elements | Iterator |
| filter() | iter.filter(predicate) |
Filter elements by predicate | Iterator |
| map() | iter.map(fn) |
Transform each element | Iterator |
| flatMap() | iter.flatMap(fn) |
Map and flatten one level | Iterator |
| reduce() | iter.reduce(fn, init) |
Reduce to single value | Any |
| forEach() | iter.forEach(fn) |
Execute function for each element | undefined |
| some() | iter.some(predicate) |
Test if any element matches | Boolean |
| every() | iter.every(predicate) |
Test if all elements match | Boolean |
| find() | iter.find(predicate) |
Find first matching element | Any or undefined |
| toArray() | iter.toArray() |
Convert to array | Array |
Lazy vs Eager Evaluation
| Category | Methods | Evaluation | Chainable |
|---|---|---|---|
| Lazy | take, drop, filter, map, flatMap | On demand, returns iterator | ✓ Yes |
| Eager | reduce, forEach, some, every, find, toArray | Immediate, returns value | ✗ No (terminal) |
Example: Polyfill implementations (pre-ES2024)
// Helper method polyfills for older environments
function* take(n, iterable) {
let count = 0;
for (const value of iterable) {
if (count++ >= n) break;
yield value;
}
}
function* drop(n, iterable) {
let count = 0;
for (const value of iterable) {
if (count++ >= n) {
yield value;
}
}
}
function* filter(predicate, iterable) {
for (const value of iterable) {
if (predicate(value)) {
yield value;
}
}
}
function* map(fn, iterable) {
for (const value of iterable) {
yield fn(value);
}
}
function* flatMap(fn, iterable) {
for (const value of iterable) {
yield* fn(value);
}
}
// Usage examples
function* naturals() {
let n = 1;
while (true) yield n++;
}
// Take first 5 numbers
const first5 = [...take(5, naturals())];
console.log(first5); // [1, 2, 3, 4, 5]
// Skip first 10, take next 5
const numbers = [...take(5, drop(10, naturals()))];
console.log(numbers); // [11, 12, 13, 14, 15]
// Filter even numbers
const evens = filter(n => n % 2 === 0, naturals());
console.log([...take(5, evens)]); // [2, 4, 6, 8, 10]
// Chain transformations (lazy evaluation)
const result = take(5,
map(n => n * n,
filter(n => n % 2 === 0, naturals())
)
);
console.log([...result]); // [4, 16, 36, 64, 100]
// FlatMap example
function* duplicateEach(n) {
yield n;
yield n;
}
const duplicated = flatMap(duplicateEach, [1, 2, 3]);
console.log([...duplicated]); // [1, 1, 2, 2, 3, 3]
Example: Terminal operations
// Reduce
function reduce(fn, initial, iterable) {
let accumulator = initial;
for (const value of iterable) {
accumulator = fn(accumulator, value);
}
return accumulator;
}
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
const sum = reduce((acc, n) => acc + n, 0, range(1, 10));
console.log(sum); // 55
// forEach (side effects)
function forEach(fn, iterable) {
for (const value of iterable) {
fn(value);
}
}
forEach(n => console.log(n), range(1, 5));
// Logs: 1, 2, 3, 4, 5
// some
function some(predicate, iterable) {
for (const value of iterable) {
if (predicate(value)) {
return true;
}
}
return false;
}
console.log(some(n => n > 5, range(1, 10))); // true
console.log(some(n => n > 20, range(1, 10))); // false
// every
function every(predicate, iterable) {
for (const value of iterable) {
if (!predicate(value)) {
return false;
}
}
return true;
}
console.log(every(n => n > 0, range(1, 10))); // true
console.log(every(n => n < 5, range(1, 10))); // false
// find
function find(predicate, iterable) {
for (const value of iterable) {
if (predicate(value)) {
return value;
}
}
return undefined;
}
console.log(find(n => n > 5, range(1, 10))); // 6
console.log(find(n => n > 20, range(1, 10))); // undefined
// toArray
function toArray(iterable) {
return [...iterable];
}
console.log(toArray(range(1, 5))); // [1, 2, 3, 4, 5]
Example: Practical use cases
// Process large dataset lazily
function* readLargeFile() {
// Simulate reading millions of lines
for (let i = 1; i <= 1000000; i++) {
yield `Line ${i}`;
}
}
// Only processes first 100 matching lines (lazy!)
const validLines = take(100,
filter(line => line.includes('error'),
map(line => line.toLowerCase(),
readLargeFile()
)
)
);
// No processing until we iterate
for (const line of validLines) {
console.log(line);
}
// Pagination helper
function* paginate(items, pageSize) {
for (let i = 0; i < items.length; i += pageSize) {
yield items.slice(i, i + pageSize);
}
}
const data = [...range(1, 100)];
const pages = paginate(data, 10);
// Get specific page
const page3 = [...take(1, drop(2, pages))][0];
console.log(page3); // [21, 22, 23, ..., 30]
// Sliding window
function* window(size, iterable) {
const buffer = [];
for (const value of iterable) {
buffer.push(value);
if (buffer.length === size) {
yield [...buffer];
buffer.shift();
}
}
}
const windows = [...window(3, [1, 2, 3, 4, 5])];
console.log(windows);
// [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
// Batch processing
function* batch(size, iterable) {
let batch = [];
for (const value of iterable) {
batch.push(value);
if (batch.length === size) {
yield batch;
batch = [];
}
}
if (batch.length > 0) {
yield batch;
}
}
const batches = [...batch(3, range(1, 10))];
console.log(batches);
// [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
// Zip iterators
function* zip(...iterables) {
const iterators = iterables.map(it => it[Symbol.iterator]());
while (true) {
const results = iterators.map(it => it.next());
if (results.some(r => r.done)) {
break;
}
yield results.map(r => r.value);
}
}
const zipped = [...zip([1, 2, 3], ['a', 'b', 'c'], [true, false, true])];
console.log(zipped);
// [[1, 'a', true], [2, 'b', false], [3, 'c', true]]
// Enumerate (with index)
function* enumerate(iterable) {
let index = 0;
for (const value of iterable) {
yield [index++, value];
}
}
const enumerated = [...enumerate(['a', 'b', 'c'])];
console.log(enumerated);
// [[0, 'a'], [1, 'b'], [2, 'c']]
Example: Composing iterator helpers
// Create reusable iterator pipelines
class IteratorPipeline {
constructor(iterable) {
this.iterable = iterable;
}
take(n) {
this.iterable = take(n, this.iterable);
return this;
}
drop(n) {
this.iterable = drop(n, this.iterable);
return this;
}
filter(predicate) {
this.iterable = filter(predicate, this.iterable);
return this;
}
map(fn) {
this.iterable = map(fn, this.iterable);
return this;
}
flatMap(fn) {
this.iterable = flatMap(fn, this.iterable);
return this;
}
// Terminal operations
toArray() {
return [...this.iterable];
}
reduce(fn, initial) {
return reduce(fn, initial, this.iterable);
}
forEach(fn) {
forEach(fn, this.iterable);
}
some(predicate) {
return some(predicate, this.iterable);
}
every(predicate) {
return every(predicate, this.iterable);
}
find(predicate) {
return find(predicate, this.iterable);
}
}
// Helper function
function pipeline(iterable) {
return new IteratorPipeline(iterable);
}
// Usage
const result1 = pipeline(naturals())
.filter(n => n % 2 === 0)
.map(n => n ** 2)
.take(5)
.toArray();
console.log(result1); // [4, 16, 36, 64, 100]
// Complex pipeline
const result2 = pipeline(range(1, 100))
.filter(n => n % 3 === 0)
.drop(5)
.map(n => n * 2)
.take(10)
.reduce((sum, n) => sum + n, 0);
console.log(result2); // Sum of processed values
// Find first match
const found = pipeline(naturals())
.filter(n => n % 7 === 0)
.find(n => n > 100);
console.log(found); // 105
// Test condition
const hasLarge = pipeline(range(1, 10))
.map(n => n ** 2)
.some(n => n > 50);
console.log(hasLarge); // true
Key Points: Iterator helpers enable functional programming
patterns
with lazy evaluation. Lazy methods (take, drop, filter, map) return iterators. Eager methods (reduce, forEach,
some) consume iterator immediately. Chain helpers for complex transformations. Perfect for
large datasets - only processes needed elements. ES2024+ adds native support.
Section 18 Summary: Iterators, Generators, and Iteration Protocols
- Iterator Protocol: Object with next() returning {value, done}
- Iterable Protocol: Object with [Symbol.iterator] method
- Built-in Iterables: Array, String, Map, Set, TypedArray, arguments
- Generators: function* syntax, pausable execution with yield
- yield Expressions: yield value, yield* delegation, two-way communication
- Generator Methods: next(), return(), throw()
- Async Generators: async function*, for await...of loops
- Custom Iterators: Implement [Symbol.iterator], track state, optional cleanup
- Iterator Helpers: take, drop, filter, map, reduce (lazy evaluation)
- Lazy Evaluation: Process data on demand, efficient for infinite sequences
- Use Cases: Streaming data, pagination, infinite sequences, resource management