51. Iterators & Generators: Lazy Evaluation in JavaScript
Introduction
An iterator is an object that defines a way to traverse a data collection one item at a time. A generator is a special function that can pause and resume its execution, producing a sequence of values lazily. Together, they enable the lazy evaluation pattern where data is processed only when needed.
For IT students working with large datasets such as the data of an entire student cohort, iterators and generators are very efficient because you don't need to load all the data into memory at once.
Why Are Iterators and Generators Important?
When working with large datasets (thousands of student records, class schedules, grade transcripts), loading everything into memory at once can cause crashes or slowness. Generators let us process data one item at a time only when it is needed (lazy evaluation), saving memory and time.
Iterator Protocol
// An iterator must have a next() method that returns { value, done }
// Manual iterator for an array
function createArrayIterator(arr) {
let index = 0;
return {
next() {
if (index < arr.length) {
return { value: arr[index++], done: false };
}
return { value: undefined, done: true };
}
};
}
const arr = [10, 20, 30];
const iter = createArrayIterator(arr);
console.log(iter.next()); // { value: 10, done: false }
console.log(iter.next()); // { value: 20, done: false }
console.log(iter.next()); // { value: 30, done: false }
console.log(iter.next()); // { value: undefined, done: true }
Symbol.iterator
// Make an object iterable with for...of
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 };
}
};
}
toArray() {
return [...this];
}
}
// Usage
const range = new Range(1, 10);
for (const num of range) {
console.log(num); // 1, 2, 3, ..., 10
}
const evenRange = new Range(2, 20, 2);
console.log([...evenRange]); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
console.log(range.toArray()); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Iterator for tree traversal
class TreeNode {
constructor(value, children = []) {
this.value = value;
this.children = children;
}
// Depth-first iterator
[Symbol.iterator]() {
const stack = [this];
return {
next() {
while (stack.length > 0) {
const node = stack.pop();
// Push children in reverse for left-to-right traversal
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
return { value: node.value, done: false };
}
return { value: undefined, done: true };
}
};
}
}
// Campus tree
const root = new TreeNode('University', [
new TreeNode('Faculty of Engineering', [
new TreeNode('Computer Science'),
new TreeNode('Information Systems'),
new TreeNode('Computer Engineering')
]),
new TreeNode('Faculty of Science', [
new TreeNode('Mathematics'),
new TreeNode('Physics')
])
]);
for (const dept of root) {
console.log(dept);
}
// University, Faculty of Engineering, Computer Science, Information Systems, Computer Engineering, Faculty of Science, Mathematics, Physics
Generator Functions
// Generators use function* syntax
function* countTo(n) {
for (let i = 1; i <= n; i++) {
yield i; // Pause and return the value
}
}
// Usage
const counter = countTo(5);
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { value: 4, done: false }
console.log(counter.next()); // { value: 5, done: false }
console.log(counter.next()); // { value: undefined, done: true }
// Can be used in for...of
for (const num of countTo(5)) {
console.log(num); // 1, 2, 3, 4, 5
}
// Generator with a parameter
function* paginate(items, pageSize) {
let page = 0;
while (page * pageSize < items.length) {
const start = page * pageSize;
const end = start + pageSize;
yield {
page: page + 1,
data: items.slice(start, end),
totalPages: Math.ceil(items.length / pageSize)
};
page++;
}
}
// Usage
const allStudents = [
{ studentId: '001', name: 'Mike' },
{ studentId: '002', name: 'John' },
{ studentId: '003', name: 'Anna' },
{ studentId: '004', name: 'Diana' },
{ studentId: '005', name: 'Eric' },
{ studentId: '006', name: 'Frank' }
];
const pages = paginate(allStudents, 3);
for (const page of pages) {
console.log(`Page ${page.page}/${page.totalPages}`);
console.log(page.data);
}
yield and next() with Values
// Generators can receive values through next()
function* conversation() {
const name = yield 'What is your name?';
const age = yield `Hello ${name}, what is your age?`;
yield `${name}, age ${age} years. Thank you!`;
}
const chat = conversation();
console.log(chat.next()); // { value: 'What is your name?', done: false }
console.log(chat.next('Mike')); // { value: 'Hello Mike, what is your age?', done: false }
console.log(chat.next(20)); // { value: 'Mike, age 20 years. Thank you!', done: false }
console.log(chat.next()); // { value: undefined, done: true }
// Generator for a state machine
function* enrollmentProcess() {
console.log('Starting registration...');
const data = yield 'FILL_DATA';
if (!data.studentId || !data.name) {
return 'ERROR: Incomplete data';
}
const confirm = yield `Confirm: ${data.name} (${data.studentId}). Continue? (y/n)`;
if (confirm !== 'y') {
return 'Cancelled';
}
yield 'PAYMENT_PROCESS';
const pay = yield 'Payment confirmation done? (y/n)';
if (pay !== 'y') {
return 'Awaiting payment';
}
return 'REGISTRATION_SUCCESS';
}
const process = enrollmentProcess();
let result = process.next();
console.log(result.value); // 'FILL_DATA'
result = process.next({ studentId: '1234567890', name: 'Mike' });
console.log(result.value); // 'Confirm: Mike (1234567890). Continue? (y/n)'
result = process.next('y');
console.log(result.value); // 'PAYMENT_PROCESS'
result = process.next();
console.log(result.value); // 'Payment confirmation done? (y/n)'
result = process.next('y');
console.log(result.value); // 'REGISTRATION_SUCCESS'
Delegation with yield*
// yield* delegates to another iterable
function* allCourses() {
yield* semester1();
yield* semester2();
yield* semester3();
}
function* semester1() {
yield 'Algorithms';
yield 'Programming Dasar';
yield 'Mathematics Diskrit';
}
function* semester2() {
yield 'Web Programming';
yield 'Database';
yield 'Computer Networking';
}
function* semester3() {
yield 'Artificial Intelligence';
yield 'Software Engineering';
}
for (const course of allCourses()) {
console.log(course);
}
// yield* with a recursive generator
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // Recursive delegation
} else {
yield item;
}
}
}
const nested = [1, [2, 3], [4, [5, 6]], 7];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5, 6, 7]
// Generator for merging multiple iterables
function* merge(...iterables) {
for (const iterable of iterables) {
yield* iterable;
}
}
const a = [1, 2, 3];
const b = [4, 5, 6];
const c = [7, 8, 9];
console.log([...merge(a, b, c)]); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
Async Iterators & Async Generators
// Async iterator protocol: next() returns a Promise
// Async generator: function* with async and await
async function* fetchStudents(page = 1, pageSize = 10) {
let currentPage = page;
while (true) {
const response = await fetch(
`/api/students?page=${currentPage}&size=${pageSize}`
);
const data = await response.json();
if (data.students.length === 0) break;
for (const student of data.students) {
yield student;
}
if (!data.hasMore) break;
currentPage++;
}
}
// for await...of for async iterables
async function displayAllStudents() {
for await (const student of fetchStudents()) {
console.log(`${student.name} - ${student.studentId}`);
}
}
// Async generator for streaming data
async function* readLargeFile(filePath) {
const response = await fetch(`/api/files/${filePath}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep the incomplete remainder
for (const line of lines) {
if (line.trim()) {
yield JSON.parse(line);
}
}
}
// Process the remaining buffer
if (buffer.trim()) {
yield JSON.parse(buffer);
}
}
// Usage
async function processFile() {
for await (const record of readLargeFile('data-student.jsonl')) {
console.log(`Processing: ${record.name}`);
}
}
Real-World Example: Iterating a Student Dataset with Lazy Evaluation
// =====================================================
// Lazy Student Dataset Iterator
// =====================================================
class StudentDataset {
#students;
constructor(students) {
this.#students = students;
}
// Lazy filter - doesn't create a new array
*where(predicate) {
for (const student of this.#students) {
if (predicate(student)) {
yield student;
}
}
}
// Lazy map - transform one at a time
*select(transform) {
for (const student of this.#students) {
yield transform(student);
}
}
// Lazy take - take the first n
*take(n) {
let count = 0;
for (const student of this.#students) {
if (count >= n) return;
yield student;
count++;
}
}
// Lazy skip - skip the first n
*skip(n) {
let count = 0;
for (const student of this.#students) {
if (count >= n) {
yield student;
}
count++;
}
}
// Chunk - split into batches
*chunk(size) {
let chunk = [];
for (const student of this.#students) {
chunk.push(student);
if (chunk.length === size) {
yield chunk;
chunk = [];
}
}
if (chunk.length > 0) yield chunk;
}
// Aggregate operations
count() {
let n = 0;
for (const _ of this.#students) n++;
return n;
}
first() {
for (const student of this.#students) {
return student;
}
return null;
}
reduce(fn, initial) {
let acc = initial;
for (const student of this.#students) {
acc = fn(acc, student);
}
return acc;
}
toArray() {
return [...this.#students];
}
// Static factory from large data
static fromGenerator(generator) {
return new StudentDataset(generator);
}
}
// Generator for student data from a "database"
function* studentDatabase(query = {}) {
// Simulate incremental fetching from a database
const allStudents = [
{ studentId: '001', name: 'Mike', majorCode: 'CS', gpa: 3.75, year: 2021 },
{ studentId: '002', name: 'John', majorCode: 'IS', gpa: 3.50, year: 2021 },
{ studentId: '003', name: 'Anna', majorCode: 'CS', gpa: 3.90, year: 2022 },
{ studentId: '004', name: 'Diana', majorCode: 'CE', gpa: 3.20, year: 2022 },
{ studentId: '005', name: 'Eric', majorCode: 'CS', gpa: 3.85, year: 2021 },
{ studentId: '006', name: 'Frank', majorCode: 'IS', gpa: 3.10, year: 2023 },
{ studentId: '007', name: 'Grace', majorCode: 'CS', gpa: 3.95, year: 2023 },
{ studentId: '008', name: 'Henry', majorCode: 'CE', gpa: 2.80, year: 2021 },
{ studentId: '009', name: 'Irene', majorCode: 'IS', gpa: 3.60, year: 2022 },
{ studentId: '010', name: 'Jack', majorCode: 'CS', gpa: 3.40, year: 2023 }
];
for (const student of allStudents) {
// Simulate a fetch delay
// In a real case, this could yield from a database cursor
if (query.majorCode && student.majorCode !== query.majorCode) continue;
if (query.year && student.year !== query.year) continue;
yield student;
}
}
// =====================================================
// Usage
// =====================================================
const dataset = StudentDataset.fromGenerator(studentDatabase());
// Lazy pipeline - only process what is needed
const topTIStudents = dataset
.where(s => s.majorCode === 'CS')
.where(s => s.gpa >= 3.5)
.select(s => ({
name: s.name,
studentId: s.studentId,
gpa: s.gpa,
status: s.gpa >= 3.8 ? 'Cum Laude' : 'Regular'
}));
// Data is not processed until we iterate
for (const student of topTIStudents) {
console.log(`${student.name} (${student.studentId}) - GPA: ${student.gpa} [${student.status}]`);
}
// Another example: lazy pagination
const allStudents = StudentDataset.fromGenerator(studentDatabase());
let pageNum = 1;
for (const page of allStudents.chunk(3)) {
console.log(`\n=== Page ${pageNum} ===`);
page.forEach(s => console.log(`${s.name} - ${s.majorCode}`));
pageNum++;
}
// Filter and count without creating a new array
const countCS = StudentDataset
.fromGenerator(studentDatabase({ majorCode: 'CS' }))
.count();
console.log(`\nNumber of CS students: `);
Example: Infinite Sequence with a Generator
// Fibonacci generator - infinite sequence
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Take the first 10 Fibonacci numbers
const fib = fibonacci();
const first10 = [];
for (let i = 0; i < 10; i++) {
first10.push(fib.next().value);
}
console.log(first10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// Auto-increment ID generator
function* idGenerator(prefix = 'ID') {
let id = 1;
while (true) {
yield `${prefix}-${String(id).padStart(5, '0')}`;
id++;
}
}
const ids = idGenerator('STU');
console.log(ids.next().value); // STU-00001
console.log(ids.next().value); // STU-00002
// Pagination generator for fetching data
async function* paginatedFetch(baseUrl, pageSize = 20) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}&size=${pageSize}`);
const data = await response.json();
yield* data.items; // Delegate items
hasMore = data.hasNextPage;
page++;
}
}
// Batch processor using a generator
function* batchProcessor(items, batchSize, processFn) {
let batch = [];
for (const item of items) {
batch.push(item);
if (batch.length === batchSize) {
yield processFn(batch);
batch = [];
}
}
if (batch.length > 0) {
yield processFn(batch);
}
}
// Usage: batch insert into a database
const studentsToInsert = [
{ studentId: '001', name: 'Mike' },
{ studentId: '002', name: 'John' },
{ studentId: '003', name: 'Anna' },
{ studentId: '004', name: 'Diana' },
{ studentId: '005', name: 'Eric' }
];
for (const batch of batchProcessor(studentsToInsert, 2, (batch) => {
console.log(`Inserting batch:`, batch.map(s => s.name));
return batch;
})) {
// Process the batch
}
// Output:
// Inserting batch: ['Mike', 'John']
// Inserting batch: ['Anna', 'Diana']
// Inserting batch: ['Eric']
Example: Async Data Pipeline
// Async generator for stream processing
async function* fetchStudentPages(baseUrl, pageSize = 10) {
let page = 1;
let hasMore = true;
while (hasMore) {
console.log(`Fetching page ${page}...`);
const response = await fetch(`${baseUrl}?page=${page}&size=${pageSize}`);
const data = await response.json();
yield* data.students; // Yield each student
hasMore = data.hasMore;
page++;
// Delay between pages to avoid overloading the server
await new Promise(resolve => setTimeout(resolve, 100));
}
}
// Async iterator with a filter
async function* filterAsync(asyncIter, predicate) {
for await (const item of asyncIter) {
if (predicate(item)) {
yield item;
}
}
}
// Async iterator with a transform
async function* mapAsync(asyncIter, transform) {
for await (const item of asyncIter) {
yield transform(item);
}
}
// Pipeline: fetch -> filter -> transform -> collect
async function getActiveCSStudents(baseUrl) {
const results = [];
const pipeline = mapAsync(
filterAsync(
fetchStudentPages(baseUrl),
s => s.majorCode === 'CS' && s.status === 'active'
),
s => ({
name: s.name,
studentId: s.studentId,
average: s.grade.reduce((a, b) => a + b, 0) / s.grade.length
})
);
for await (const student of pipeline) {
results.push(student);
}
return results;
}
// Usage
const csStudents = await getActiveCSStudents('/api/students');
console.log(`Found ${csStudents.length} active CS students`);
Code Explanation
Iterator Protocol
- An object must have a
next()method that returns{ value, done } done: truesignals that iteration is complete- Can be iterated with
for...of, spread[...], and destructuring
Symbol.iterator
- A special method that makes an object iterable
- Can be used with
for...ofand the spread operator - Returns an iterator object with a
next()method
Generator Functions
- Declared with
function* yieldto pause and return a valueyield*to delegate to another iterable- Returns an iterator that can be called with
next() - Can receive values through
next(value)
Async Generators
- A combination of
async function* yieldworks with Promises- Can be iterated with
for await...of - Suited for data streaming or pagination
Conclusion
Iterators and generators are powerful features for lazy evaluation and sequence processing in JavaScript. With generators, you can process large datasets such as the data of an entire student cohort one item at a time without loading everything into memory. Async generators allow streaming data from a server efficiently. For a campus application, use generators for data pagination, batch processing, and lazy loading that keep performance optimal even with datasets of thousands of students.