In JavaScript, iterators allow you to define how a set of data should be iterated over.
While JavaScript provides built-in iterators for arrays, strings, and other objects, you can also create user-defined iterators to customize iteration for custom data structures.
In this tutorial, we will cover:
- What are Iterators?
- The Iterator Protocol
- Creating User-Defined Iterators
- Using Iterators with for…of Loops
- Generators as a Simplified Way to Create Iterators
- Examples and Use Cases
Let’s dive into each section with examples.
1. What are Iterators?
An iterator is an object that allows you to traverse through a collection of data one element at a time. It provides a way to step through the items in a collection without needing to know the underlying structure.
Built-in Iterators:
In JavaScript, arrays, strings, and other built-in data types have their own iterators. For example:
const array = [1, 2, 3]; const iterator = array[Symbol.iterator](); 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 }
Here, the next() method returns the next item in the iteration, and once all items are consumed, it returns { value: undefined, done: true }.
2. The Iterator Protocol
To create a user-defined iterator, you need to implement the iterator protocol. An object is an iterator if it implements the next() method, which returns an object with two properties:
- value: The next value in the sequence.
- done: A boolean that is true when the iterator is finished and false otherwise.
Example: Basic Iterator
function createIterator(array) { let index = 0; return { next() { if (index < array.length) { return { value: array[index++], done: false }; } else { return { value: undefined, done: true }; } } }; } const myIterator = createIterator([10, 20, 30]); console.log(myIterator.next()); // { value: 10, done: false } console.log(myIterator.next()); // { value: 20, done: false } console.log(myIterator.next()); // { value: 30, done: false } console.log(myIterator.next()); // { value: undefined, done: true }
In this example, the createIterator() function returns an object with a next() method. The iterator traverses through the array and keeps track of the index.
3. Creating User-Defined Iterators
User-defined iterators can be particularly useful when you need to iterate over complex data structures or apply custom logic to the iteration process.
Example: User-Defined Iterator for a Custom Object
Let's create a custom object that represents a range of numbers and implement a custom iterator for it.
const range = { start: 1, end: 5, [Symbol.iterator]() { let current = this.start; const last = this.end; return { next() { if (current <= last) { return { value: current++, done: false }; } else { return { value: undefined, done: true }; } } }; } }; const rangeIterator = range[Symbol.iterator](); console.log(rangeIterator.next()); // { value: 1, done: false } console.log(rangeIterator.next()); // { value: 2, done: false } console.log(rangeIterator.next()); // { value: 3, done: false } console.log(rangeIterator.next()); // { value: 4, done: false } console.log(rangeIterator.next()); // { value: 5, done: false } console.log(rangeIterator.next()); // { value: undefined, done: true }
In this example:
- The range object defines the range of numbers (start and end).
- The [Symbol.iterator]() method is used to define the iterator for the object.
- The iterator object uses next() to iterate from start to end.
4. Using Iterators with for…of Loops
JavaScript’s for…of loop works with any object that implements the iterator protocol. This makes it easy to iterate over collections like arrays, strings, and user-defined iterators.
Example: Iterating Over a Custom Object with for…of
Using the range object from the previous example:
for (const num of range) { console.log(num); // Output: 1, 2, 3, 4, 5 }
In this example, the for…of loop automatically calls the next() method on the iterator and continues until done is true.
5. Generators as a Simplified Way to Create Iterators
Generators are special functions in JavaScript that can be used to create iterators more easily. They are defined using the function* syntax and allow you to yield values one at a time.
Example: Using a Generator to Create an Iterator
function* numberGenerator(start, end) { for (let i = start; i <= end; i++) { yield i; // Yield each number in the range } } const numbers = numberGenerator(1, 5); console.log(numbers.next()); // { value: 1, done: false } console.log(numbers.next()); // { value: 2, done: false } console.log(numbers.next()); // { value: 3, done: false } console.log(numbers.next()); // { value: 4, done: false } console.log(numbers.next()); // { value: 5, done: false } console.log(numbers.next()); // { value: undefined, done: true }
In this example:
- The generator numberGenerator() produces a sequence of numbers from start to end using yield.
- Each time next() is called, the generator produces the next value until the sequence is complete.
Example: Iterating with for…of Using Generators
function* numberGenerator(start, end) { for (let i = start; i <= end; i++) { yield i; } } for (const num of numberGenerator(1, 5)) { console.log(num); // Output: 1, 2, 3, 4, 5 }
Generators make it much easier to implement iterators without explicitly managing the state of the iterator.
6. Examples and Use Cases
Example 1: Fibonacci Sequence Iterator
Let’s create an iterator that generates the Fibonacci sequence.
function* fibonacci(limit) { let a = 0, b = 1; for (let i = 0; i < limit; i++) { yield a; [a, b] = [b, a + b]; } } const fibSequence = fibonacci(7); for (const num of fibSequence) { console.log(num); // Output: 0, 1, 1, 2, 3, 5, 8 }
In this example, the generator function fibonacci() generates the Fibonacci sequence up to a given limit using yield.
Example 2: Paginated Data Iterator
Imagine you are fetching paginated data from an API, and you want to iterate over all pages of data using an iterator.
const paginatedData = { currentPage: 1, totalPages: 3, data: [ ['Item 1', 'Item 2'], ['Item 3', 'Item 4'], ['Item 5', 'Item 6'] ], [Symbol.iterator]() { let page = this.currentPage; const lastPage = this.totalPages; return { next: () => { if (page <= lastPage) { return { value: this.data[page++ - 1], done: false }; } else { return { value: undefined, done: true }; } } }; } }; for (const pageData of paginatedData) { console.log('Page Data:', pageData); } // Output: // Page Data: [ 'Item 1', 'Item 2' ] // Page Data: [ 'Item 3', 'Item 4' ] // Page Data: [ 'Item 5', 'Item 6' ]
In this example, we implement a paginated data iterator that returns data page by page.
Summary of Key Concepts for User-Defined Iterators
Concept | Description |
---|---|
Iterator Protocol | Objects that implement the next() method and return an object with value and done properties. |
User-Defined Iterators | Custom objects implementing the iterator protocol, often using [Symbol.iterator]() to make them iterable. |
Generators | Special functions (function*) that use yield to produce values lazily, simplifying the creation of iterators. |
for…of Loop | Automatically calls the iterator’s next() method and loops through values until done is true. |
Symbol.iterator | A method that returns the iterator object and makes custom objects iterable (allowing use with for…of and other iteration constructs). |
Yielding Values with yield | In generator functions, yield produces a value and pauses execution until the next value is requested (via next()). |
Conclusion
In this tutorial, we explored how to create user-defined iterators in JavaScript, making it possible to define custom iteration logic for objects and collections. We covered:
- The iterator protocol and how to implement it.
- Using iterators with for…of loops for easier iteration.
- Simplifying the creation of iterators with generators.
- Several practical examples such as Fibonacci sequences and paginated data.