Iterators and generators in JavaScript
- 2020-03-30 04:13:41
- OfStack
Handling each item in a collection is a very general operation, and JavaScript provides many ways to iterate over a collection, from simple for and for each loop to map (), filter (), and array comprehensions. In JavaScript 1.7, iterators and generators introduce a new iteration mechanism in the JavaScript core syntax, but also provide customization for... Mechanisms of in and for each circular behavior.
The iterator
An iterator is an object that accesses an element in a sequence of collections each time and tracks the current position of the iteration in that sequence. In JavaScript, an iterator is an object that provides a next() method that returns the next element in the sequence. This method raises a StopIteration exception when all the elements in the sequence are iterated over.
Once the iterator object is created, you can either call next() explicitly and repeatedly, or use JavaScript's for... The in and for each loops are implicitly called.
Simple iterators that iterate over objects and arrays can be created using the Iterator() :
var lang = { name: 'JavaScript', birthYear: 1995 };
var it = Iterator(lang);
Once the initialization is complete, the next() method can be called to access the object's key-value pairs in turn:
var pair = it.next(); //The key-value pair is ["name", "JavaScript"]
pair = it.next(); //The key-value pair is ["birthday", 1995]
pair = it.next(); //An 'StopIteration' exception is thrown
The for... The in loop can be used to replace an explicit call to the next() method. When a StopIteration exception is thrown, the loop automatically terminates.
var it = Iterator(lang);
for (var pair in it)
print(pair); //Output one [key, value] key-value pair in it at a time
If you only want to iterate over the key value of the object, you can pass the second parameter to the Iterator() function, which is true:
var it = Iterator(lang, true);
for (var key in it)
print(key); //Output the key value
each time
One advantage of using Iterator() to access objects is that the custom properties added to object.prototype are not included in the sequence Object.
Iterator() can also be applied to an array:
var langs = ['JavaScript', 'Python', 'Haskell'];
var it = Iterator(langs);
for (var pair in it)
print(pair); //Each iteration outputs [index, language] key-value pairs
Just like traversing an object, passing true as the second argument will result in an array index:
var langs = ['JavaScript', 'Python', 'Haskell'];
var it = Iterator(langs, true);
for (var i in it)
print(i); //Output 0, then 1, then 2
The let keyword is used to assign indexes and values to block variables separately within the loop, and also to deconstruct assignments (Destructuring Assignment) :
var langs = ['JavaScript', 'Python', 'Haskell'];
var it = Iterators(langs);
for (let [i, lang] in it)
print(i + ': ' + lang); //Output "0: JavaScript" and so on
Declare a custom iterator
Objects representing collections of elements should be iterated in a specified manner.
1. Iterating over a Range of objects should return the Numbers contained in the Range, one by one
2. Leaf nodes of a tree can be accessed with depth-first or breadth-first access
3. Iterating over an object representing the results of a database query should return row by row, even if the entire result set has not yet been fully loaded into a single array
4. Iterators operating on an infinite mathematical sequence (like the Fibonacci sequence) should return one result after another without creating an infinite length data structure
JavaScript allows you to write custom iteration logic and apply it to an object
We create a simple Range object with two values, low and high:
function Range(low, high){
this.low = low;
this.high = high;
}
Now we create a custom iterator that returns a sequence of all integers in the range. The iterator interface requires us to provide a next() method to return the next element in the sequence or to raise a StopIteration exception.
function RangeIterator(range){
this.range = range;
this.current = this.range.low;
}
RangeIterator.prototype.next = function(){
if (this.current > this.range.high)
throw StopIteration;
else
return this.current++;
};
Our RangeIterator is instantiated by a range instance, while maintaining a current property to keep track of the current sequence position.
Finally, in order for RangeIterator to combine with Range, we need to add a special iteration ator__ method for Range. When we try to iterate over a Range, it will be called and should return an instance of RangeIterator that implements the iteration logic.
Range.prototype.__iterator__ = function(){
return new RangeIterator(this);
};
After completing our custom iterator, we can iterate over a scope instance:
var range = new Range(3, 5);
for (var i in range)
print(i); //Output 3, then 4, then 5
Generator: a better way to build iterators
While custom iterators are a useful tool, they need to be carefully planned when they are created because their internal state needs to be explicitly maintained.
The generator provides a powerful feature: it allows you to define a function with its own iterative algorithm, and it can automatically maintain its own state.
The generator is a special function that can be used as an iterator factory. If a function contains one or more yield expressions, it is called a generator.
Note: only HTML is included in < The script type = "application/javascript; Version = "1.7 > A block of code in (or later) can use the yield keyword. XUL (XML User Interface Language) script tags can access these features without specifying this particular block of code.
When a generator function is called, the body of the function does not execute immediately and returns a generator-iterator object. Each time the next() method of generator-iterator is called, the body of the function executes to the next yield expression and returns its result. When a function ends or a return statement is encountered, a StopIteration exception is thrown.
To illustrate this better, use an example:
function simpleGenerator(){
yield "first";
yield "second";
yield "third";
for (var i = 0; i < 3; i++)
yield i;
}
var g = simpleGenerator();
print(g.next()); //Output "first" < br / >
print(g.next()); //Output "second" < br / >
print(g.next()); //Output "third" < br / >
print(g.next()); //0 < br / >
print(g.next()); //Output 1 < br / >
print(g.next()); //Output 2 < br / >
print(g.next()); //Raises a StopIteration exception
The generator function can be used as a class directly with the iteration ator__ method, effectively reducing the amount of code where custom iterators are required. We use the generator to rewrite the Range:
function Range(low, high){
this.low = low;
this.high = high;
}
Range.prototype.__iterator__ = function(){
for (var i = this.low; i <= this.high; i++)
yield i;
};
var range = new Range(3, 5);
for (var i in range)
print(i); //Output 3, then 4, then 5
Not all generators will terminate, you can create a generator that represents an infinite sequence. The following generator implements a Fibonacci sequence in which each element is the sum of the previous two:
function fibonacci(){
var fn1 = 1;
var fn2 = 1;
while (1) {
var current = fn2;
fn2 = fn1;
fn1 = fn1 + current;
yield current;
}
}
var sequence = fibonacci();
print(sequence.next()); // 1
print(sequence.next()); // 1
print(sequence.next()); // 2
print(sequence.next()); // 3
print(sequence.next()); // 5
print(sequence.next()); // 8
print(sequence.next()); // 13
Generator functions can take arguments and use them the first time the function is called. A generator can be terminated (causing it to raise a StopIteration exception) by using a return statement. The following Fibonacci () variant takes an optional limit parameter that terminates the function when a condition is triggered.
function fibonacci(limit){
var fn1 = 1;
var fn2 = 1;
while(1){
var current = fn2;
fn2 = fn1;
fn1 = fn1 + current;
if (limit && current > limit)
return;
yield current;
}
}
Generator advanced features
The generator can calculate yield return values based on requirements, which allows it to represent previously expensive sequence computation requirements, or even the infinite sequence shown above.
In addition to the next() method, the generator-iterator object has a send() method that modifiers the internal state of the generator. The value sent to send() is treated as the result of the last yield expression, and the generator is paused. Before you can use the send() method to pass a specified value, you must call next() at least once to start the generator.
The following Fibonacci generator USES the send() method to restart the sequence:
function fibonacci(){
var fn1 = 1;
var fn2 = 1;
while (1) {
var current = fn2;
fn2 = fn1;
fn1 = fn1 + current;
var reset = yield current;
if (reset) {
fn1 = 1;
fn2 = 1;
}
}
}
var sequence = fibonacci();
print(sequence.next()); //1
print(sequence.next()); //1
print(sequence.next()); //2
print(sequence.next()); //3
print(sequence.next()); //5
print(sequence.next()); //8
print(sequence.next()); //13
print(sequence.send(true)); //1
print(sequence.next()); //1
print(sequence.next()); //2
print(sequence.next()); //3
Note: the interesting thing is that calling send(undefined) is exactly the same as calling next(). However, when the send() method is called to start a new generator, any value other than undefined throws a TypeError exception.
You can force the generator to throw an exception by calling the throw method and passing an outlier that it should throw. This exception is thrown from the current context and pauses the generator, similar to the current yield, except instead of the throw value statement.
If the yield is not encountered during the process of throwing an exception, the exception is passed until the throw() method is called, and a subsequent call to next() causes the StopIteration exception to be thrown.
The generator has a close() method to force the generator to end. Ending a generator has the following effects:
1. The valid finally sentence inall generators will be executed
2. If the finally sentence raises any exception except StopIteration, the exception is passed to the caller of the close() method
The generator will terminate
Generator expression
(link: https://developer.mozilla.org/en/JavaScript/Guide/Predefined_Core_Objects#Array_comprehensions), an obvious disadvantage is that they lead to the whole array structure in memory. The overhead is trivial when the input to the derivation itself is a small array - however, problems can arise when the input array is large or when creating a new expensive (or infinite) array generator.
The generator allows lazy computation to calculate elements as needed. The generator expression is syntactically almost identical to the array derivation -- it replaces square brackets with parentheses (and USES for... In for each... In) - but it creates a generator instead of an array so that the computation can be deferred. You can think of it as a short syntax for creating a generator.
Suppose we have an iterator it to iterate over a huge sequence of integers. We need to create a new iterator to iterate over even Numbers. An array derivation will create an entire array of even Numbers in memory:
var doubles = [i * 2 for (i in it)];
The generator expression will create a new iterator and calculate the even value as needed when needed:
var it2 = (i * 2 for (i in it));
print(it2.next()); //The first even number in it
print(it2.next()); //The second even number in it
When a generator is used as an argument to a function, the parentheses are used as a function call, meaning that the outermost parentheses can be omitted:
var result = doSomething(i * 2 for (i in it));
The End.