Back to Articles
22 min read

JavaScript Advanced Flows: Iterators, Generators, and Keyed Collections

Standard Arrays and Objects are not always the right tools for the job. This deep dive introduces the specialized efficiency of Maps and Sets, alongside the memory-safety mechanisms of WeakMaps. Furthermore, we deconstruct the Iterator Protocol and Generator functions, empowering you to implement lazy evaluation, custom traversal logic, and infinite sequences.

Iterators and Generators

Iteration Protocols

Iteration protocols are conventions that define how objects can be iterated in JavaScript, consisting of two protocols: the iterable protocol (object can be iterated) and the iterator protocol (how to produce sequence of values). These protocols enable objects to work with for...of, spread operator, and destructuring.


Iterable Protocol

An object is iterable when it implements a method at Symbol.iterator that returns an iterator object; built-in iterables include Array, String, Map, Set.

const arr = [1, 2, 3]; console.log(typeof arr[Symbol.iterator]); // "function"

Iterator Protocol

An object is an iterator when it implements a next() method that returns { value: any, done: boolean }.

const iterator = [1, 2][Symbol.iterator](); console.log(iterator.next()); // { value: 1, done: false } console.log(iterator.next()); // { value: 2, done: false } console.log(iterator.next()); // { value: undefined, done: true }

for-of Loop with Iterables

The for...of loop iterates over iterable objects, automatically calling Symbol.iterator and next() until done: true.

for (const char of "Hi") { console.log(char); // "H", "i" }

Spread with Iterables

The spread operator ... works with any iterable, expanding its values into individual elements.

const set = new Set([1, 2, 3]); const arr = [...set]; // [1, 2, 3] const str = [..."hello"]; // ['h','e','l','l','o']

Symbol.iterator

Symbol.iterator is a well-known symbol that specifies the default iterator for an object; accessing obj[Symbol.iterator] returns the iterator factory function.

┌─────────────────────────────────────────────┐
│  Iterable Object                            │
│  ┌────────────────────────────────────────┐ │
│  │ [Symbol.iterator]: function() {        │ │
│  │     return iterator;                   │ │
│  │ }                                      │ │
│  └────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

Custom Iterables

You can make any object iterable by implementing Symbol.iterator that returns an object with a next() method.

const range = { start: 1, end: 3, [Symbol.iterator]() { let current = this.start; const end = this.end; return { next() { return current <= end ? { value: current++, done: false } : { done: true }; } }; } }; console.log([...range]); // [1, 2, 3]

Generator Functions

Generator functions (declared with function*) return a generator object that conforms to both iterable and iterator protocols, enabling pausable/resumable execution.

function* gen() { yield 1; yield 2; } const g = gen(); console.log([...g]); // [1, 2]

yield Keyword

The yield keyword pauses generator execution and returns a value; execution resumes from that point on the next next() call.

function* counter() { yield 1; yield 2; return 3; // final value, done: true }

yield*

The yield* expression delegates to another iterable or generator, yielding each of its values in sequence.

function* concat() { yield* [1, 2]; yield* [3, 4]; } console.log([...concat()]); // [1, 2, 3, 4]

Generator.prototype.next

The next(value) method resumes generator execution; the optional value argument becomes the result of the paused yield expression.

function* dialog() { const name = yield "What's your name?"; yield `Hello, ${name}!`; } const g = dialog(); console.log(g.next().value); // "What's your name?" console.log(g.next("Alice").value); // "Hello, Alice!"

Generator.prototype.return

The return(value) method terminates the generator and returns { value, done: true }, triggering any finally blocks.

function* gen() { try { yield 1; yield 2; } finally { console.log("cleanup"); } } const g = gen(); g.next(); // { value: 1, done: false } g.return("end"); // logs "cleanup", returns { value: "end", done: true }

Generator.prototype.throw

The throw(error) method throws an error inside the generator at the paused yield, allowing error handling within the generator.

function* gen() { try { yield 1; } catch (e) { console.log("Caught:", e); } } const g = gen(); g.next(); g.throw("Error!"); // logs "Caught: Error!"

Async Generators

Async generators combine async and function*, allowing yield of promises; they return an async iterator with next() returning a promise.

async function* fetchPages() { yield await fetch('/page1').then(r => r.json()); yield await fetch('/page2').then(r => r.json()); }

for-await-of

The for await...of loop iterates over async iterables, awaiting each promise before continuing to the next iteration.

async function process() { for await (const page of fetchPages()) { console.log(page); } }

Maps and Sets

Map

A Map is a built-in JavaScript collection that stores key-value pairs where keys can be of any type (objects, functions, primitives), unlike regular objects which coerce keys to strings. Maps maintain insertion order and provide better performance for frequent additions/deletions.

const map = new Map(); map.set({id: 1}, 'user1'); // object as key map.set(42, 'answer'); // number as key map.set(() => {}, 'func'); // function as key

Map Methods (set, get, has, delete, clear)

These are the core methods for manipulating Map entries: set(key, value) adds/updates, get(key) retrieves, has(key) checks existence, delete(key) removes one entry, and clear() removes all entries.

const map = new Map(); map.set('name', 'Alice'); // returns Map (chainable) map.get('name'); // 'Alice' map.has('name'); // true map.delete('name'); // true (success) map.clear(); // removes all map.size; // 0

Map Iteration

Maps are iterable and maintain insertion order; you can iterate using for...of, forEach(), or access iterators via keys(), values(), and entries() methods.

const map = new Map([['a', 1], ['b', 2], ['c', 3]]); for (const [key, value] of map) { console.log(`${key}: ${value}`); } map.forEach((value, key) => console.log(key, value)); [...map.keys()] // ['a', 'b', 'c'] [...map.values()] // [1, 2, 3] [...map.entries()] // [['a',1], ['b',2], ['c',3]]

Map vs Object

Maps are preferred when keys are unknown at runtime, keys are non-strings, or when you need frequent additions/deletions; Objects are better for static structures with string keys and when you need JSON serialization.

┌─────────────────┬─────────────────────┬─────────────────────┐
│ Feature         │ Map                 │ Object              │
├─────────────────┼─────────────────────┼─────────────────────┤
│ Key types       │ Any type            │ String/Symbol only  │
│ Order           │ Insertion order     │ Complex rules       │
│ Size            │ map.size            │ Object.keys().length│
│ Iteration       │ Directly iterable   │ Need Object.keys()  │
│ Performance     │ Better for add/del  │ Better for static   │
│ JSON support    │ No direct support   │ Native support      │
│ Prototype       │ No inherited keys   │ Has prototype chain │
└─────────────────┴─────────────────────┴─────────────────────┘

WeakMap

A WeakMap holds weak references to object keys only, meaning entries are garbage-collected when no other references to the key exist; it's not iterable and has no size property.

const wm = new WeakMap(); let obj = {data: 'secret'}; wm.set(obj, 'metadata'); wm.get(obj); // 'metadata' obj = null; // Key is now eligible for garbage collection // WeakMap entry automatically removed

WeakMap Use Cases

WeakMaps excel at storing private data associated with objects, caching computed results tied to object lifetimes, and tracking DOM elements without preventing their garbage collection.

// Private data pattern const privateData = new WeakMap(); class User { constructor(name, password) { this.name = name; privateData.set(this, { password }); // truly private } checkPassword(pwd) { return privateData.get(this).password === pwd; } } // Caching expensive computations const cache = new WeakMap(); function process(obj) { if (!cache.has(obj)) { cache.set(obj, expensiveComputation(obj)); } return cache.get(obj); }

Set

A Set is a collection of unique values of any type, automatically eliminating duplicates; it maintains insertion order and provides O(1) average time complexity for add, delete, and lookup operations.

const set = new Set([1, 2, 2, 3, 3, 3]); console.log(set); // Set {1, 2, 3} console.log(set.size); // 3 // Quick array deduplication const unique = [...new Set([1, 1, 2, 2, 3])]; // [1, 2, 3]

Set Methods (add, has, delete, clear)

These core methods manage Set contents: add(value) inserts (returns Set for chaining), has(value) checks existence, delete(value) removes and returns boolean, clear() empties the Set.

const set = new Set(); set.add(1).add(2).add(3); // chainable set.add(2); // ignored (duplicate) set.has(2); // true set.delete(2); // true set.has(2); // false set.size; // 2 set.clear(); // Set is now empty

Set Operations

JavaScript Sets don't have built-in union/intersection/difference methods, but these operations are easily implemented using spread operator and array methods (ES2025 will add native methods).

const a = new Set([1, 2, 3]); const b = new Set([3, 4, 5]); // Union const union = new Set([...a, ...b]); // {1,2,3,4,5} // Intersection const intersection = new Set( [...a].filter(x => b.has(x)) ); // {3} // Difference (a - b) const difference = new Set( [...a].filter(x => !b.has(x)) ); // {1,2} // Symmetric Difference const symDiff = new Set( [...a].filter(x => !b.has(x)).concat( [...b].filter(x => !a.has(x))) ); // {1,2,4,5}

Set Iteration

Sets are directly iterable in insertion order using for...of, forEach(), or iterator methods; keys() and values() return the same iterator (values), and entries() returns [value, value] pairs for Map compatibility.

const set = new Set(['a', 'b', 'c']); for (const value of set) { console.log(value); // 'a', 'b', 'c' } set.forEach(value => console.log(value)); [...set.values()] // ['a', 'b', 'c'] [...set.keys()] // ['a', 'b', 'c'] (same as values) [...set.entries()] // [['a','a'], ['b','b'], ['c','c']]

WeakSet

A WeakSet stores only objects with weak references, allowing garbage collection when no other references exist; it's not iterable, has no size, and only supports add(), has(), and delete() methods.

const ws = new WeakSet(); let obj = {id: 1}; ws.add(obj); ws.has(obj); // true obj = null; // Object can be garbage collected // Automatically removed from WeakSet

WeakSet Use Cases

WeakSets are ideal for tagging/branding objects (marking as processed), detecting circular references, and tracking object states without preventing garbage collection.

// Tracking processed items const processed = new WeakSet(); function processOnce(obj) { if (processed.has(obj)) { return; // Already processed } processed.add(obj); // ... process object } // Branding/validation pattern const validatedUsers = new WeakSet(); function validateUser(user) { // ... validation logic validatedUsers.add(user); } function isValidated(user) { return validatedUsers.has(user); }