Visit our official platform: Navvan
Follow us on: LinkedIn | YouTube
An In-Depth Look at JavaScript's Iteration Tools ๐ ๏ธ
Iterators and generators are powerful features that provide a mechanism for customizing the behavior of forโฆof
loops. ๐
Iterators: Unlocking the Power of Iteration ๐
Before diving into generators, let's first understand iterators. Most likely, you've already used iterators when working with arrays, maps, or even strings. (Yes, ๐ฎ you've been using them!) ๐
for (let char of 'hello') {
console.log(char); // 'h', 'e', 'l', 'l', 'o'
}
Any object that implements the iterable protocol can be iterated using a forโฆof
loop. ๐ To make an object iterable, you need to implement the iterator method, which returns an iterator object. ๐งฑ
An iterator is an object that defines a sequence and potentially a return value upon termination. It has a next()
โญ๏ธ method that returns an object with two properties:
- value: The next value in the iteration sequence โฉ
-
done:
true
โ if the last value has been consumed,false
โ otherwise
Here's how you can craft your range iterator: โ๏ธ
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start;
let iterationCount = 0;
const rangeIterator = {
next() {
let result;
if (nextIndex < end) {
result = {
value: nextIndex,
done: false
};
nextIndex += step;
iterationCount++;
return result;
}
return {
value: iterationCount,
done: true
};
}
};
return rangeIterator;
}
This iterator represents a sequence of numbers from start ๐ to end ๐ with a given step ๐ช. Its next()
โญ๏ธ method returns the next number in the sequence until the end is reached.
Creating Custom Iterators ๐พ๐ค
While many built-in JS objects, such as arrays and strings, are iterable by default, you can also create custom iterators for your objects. To create a custom iterator, define a method with the key Symbol.iterator
for your object, and this method should return an iterator object with a next()
method.
For a range of numbers, you can construct a custom iterator like this:
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
};
} else {
return {
value: undefined,
done: true
};
}
},
};
}
}
const range = new Range(1, 3);
for (const number of range) {
console.log(number); // 1, 2, 3
}
While they may not be used every day, understanding how to leverage iterators and generators can be a great advantage when you encounter situations that require their unique capabilities. ๐ช
Generators: Simplifying Iterator Creationย โจ
While custom iterators are useful, creating them requires careful ๐ง state management. This is where generator functions come to the rescue! ๐ฆธโโ๏ธ
Generator functions allow you to define an iterative algorithm by writing a single ๐ function whose execution is not continuous. They are written using the function*
syntax and use the yield
keyword to pause โธ๏ธ and resume ๐ฌ their execution.
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
let iterationCount = 0;
for (let i = start; i < end; i += step) {
iterationCount++;
yield i;
}
return iterationCount;
}
When called, a generator function returns a special iterator called a Generator. Each time the generator's next()
โญ๏ธ method is called, the function executes until it encounters the next yield
expression. ๐งฎ
The yield
keyword is similar to return
, but instead of terminating the function, it pauses its execution until it encounters another next()
โญ๏ธ call, behaving exactly like a breakpoint for iteration.
function* stringGenerator() {
yield 'Iterators';
yield 'Generators';
}
const strings = stringGenerator();
console.log(strings.next()); // { value: 'Iterators', done: false }
console.log(strings.next()); // { value: 'Generators', done: false }
console.log(strings.next()); // { value: undefined, done: true }
Basic Generator Functions โ๏ธ
To create a generator function, you need to use the function*
syntax. Within this, you can use the yield
keyword to pause the execution and return a value to the caller. When the generator is resumed by calling next()
, it will continue executing from where it left off.
Here's a simple one that yields the numbers from 1 to 3:
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const generator = numberGenerator();
for (const number of generator) {
console.log(number); // 1, 2, 3
}
Nesting Generators
Since generators return iterators and those iterators are iterable, we can use yield*
to nest generators, just like we nest functions.
function* getNumbers() {
yield -3;
yield -2;
yield -1;
}
function* counterGenerator() {
let i = 0;
while (true) {
yield i;
i++;
}
}
function* getNumbersThenCount() {
yield* getNumbers();
yield* counterGenerator();
}
for (let element of getNumbersThenCount()) {
if (element > 4) {
break;
}
console.log(element);
}
Output:
-3 // <- getNumbers()
-2
-1
0 // <- counterGenerator()
1
2
3
4
Use Cases for Generators and Iterators ๐ฏ
- Represent infinite sequences: Since generators compute values on-demand, they can efficiently represent sequences of unlimited size. โพ๏ธ
- Implement custom iteration behavior: Generators allow you to define custom iteration logic for your objects, giving you fine-grained control. ๐๏ธ
- Simplify asynchronous code: Generator functions can be used to implement asynchronous iteration, allowing you to work with asynchronous data sources, such as APIs or streams, in a more streamlined way. ๐
- Implement lazy evaluation: Generators and iterators can be useful for implementing lazy evaluation, where values are only computed when they are needed. This can be particularly helpful for working with large data sets or expensive computations. ๐ฆฅ
Bonus: ๐ Endless Fibonacci Sequence with a Resetter
function* fibonacciGenerator() {
let a = 0;
let b = 1;
while (true) {
const reset = yield a;
[a, b] = [b, a + b];
if (reset) {
a = 0;
b = 1;
}
}
}
const generator = fibonacciGenerator();
// Get the first 10 Fibonacci numbers
for (let i = 0; i < 10; i++) {
if (i === 8) {
console.log(generator.next(true).value);
}
console.log(generator.next().value);
}
Output:
0
1
1
2
3
5
8
13
0 // <- resetter ran here
1
1
โ Can I use generators and iterators with other JavaScript data structures?
Yes, they can be used with a variety of JS data structures. Many built-in JS objects, like arrays, strings, and sets, already have default iterator implementations. We can also create custom iterators and generator functions for our objects and data structures.
Conclusion ๐
Generators and iterators are powerful tools in JavaScript that allow you to customize iteration behavior and create more expressive and readable code. Now that you know how they work under the hood, you can leverage their capabilities to solve complex problems elegantly.
If you enjoyed this article, please make sure to Like & Subscribe ๐
Visit our official platform: Navvan
Top comments (0)