Notes Javascript - Iterators & Generators
Iterators & Generators
Iterators and Generators bring the concept of iteration directly into the core language and provide a mechanism for customizing the behavior of for...of loops.
Iterators
In JavaScript an iterator is an object which defines a sequence and potentially a return value upon its termination. Specifically, an iterator is any object which implements the Iterator Protocol by having a next() method that returns an object with two properties:
value | The next value in the iteration sequence. |
---|---|
done | This is true if the last value in the sequence has already been consumed. If value is present alongside done, it is the iterator's return value. |
Once created, an iterator object can be iterated explicitly by repeatedly calling next(). Iterating over an iterator is said to consume the iterator, because it is generally only possible to do once. After a terminating value has been yielded additional calls to next() should continue to return {done: true}.
The most common iterator in JavaScript is the Array iterator, which returns each value in the associated array in sequence.
While it is easy to imagine that all iterators could be expressed as arrays, this is not true. Arrays must be allocated in their entirety, but iterators are consumed only as necessary. Because of this, iterators can express sequences of unlimited size, such as the range of integers between 0 and Infinity.
Here is an example which can do just that. It allows creation of a simple range iterator which defines a sequence of integers from start (inclusive) to end (exclusive) spaced step apart. Its final return value is the size of the sequence it created, tracked by the variable iterationCount.
// THE FUNCTION HAS 3 PARAMETERS WITH DEFAULT VALUES function makeRangeIterator(start = 0, end = Infinity, step = 1) { let nextIndex = start; let iterationCount = 0; const rangeIterator = { next() { let result; // A JSON OBJECT // CHECK FOR DONE CONDITION if (nextIndex < end) { // HAVE NOT REACHED THE END, SET THE value AND done VALUES result = { value: nextIndex, done: false }; // INCREMENT THE STEP (IN THIS EXAMPLE - ADD 2) nextIndex += step; // INCREMENT ITERATION COUNTER iterationCount++; // RETURN THE JSON OBJECT WHICH CONTAINS value AND done return result; } // HAVE REACHED THE END return { value: iterationCount, done: true }; }, }; return rangeIterator; } function runScript1() { let returnStr = ""; let divResult = document.getElementById("results1"); // THE FOLLOWING ITERATOR WILL START WITH 1, END ON 10, AND STEP BY 2. const myIterator = makeRangeIterator(1, 10, 2); // GET THE INITIAL ITERATOR VALUE (IN THIS CASE 1) let result = myIterator.next(); // RUN ITERATOR UNTIL DONE CONDITION IS REACHED while (!result.done) { // COPY THE RESULT TO A STRING returnStr += "<br/>"+ result.value; // INCREMENT THE ITERATOR result = myIterator.next(); } divResult.innerHTML = returnStr; }
Run Script 1Iterator Response
Generator Functions
While custom iterators are a useful tool, their creation requires careful programming due to the need to explicitly maintain their internal state. Generator functions provide a powerful alternative: they allow you to define an iterative algorithm by writing a single function whose execution is not continuous. Generator functions are written using the function* syntax.
When called, generator functions do not initially execute their code. Instead, they return a special type of iterator, called a Generator. When a value is consumed by calling the generator's next method, the Generator function executes until it encounters the yield keyword.
The function can be called as many times as desired, and returns a new Generator each time. Each Generator may only be iterated once.
We can now adapt the example from above. The behavior of this code is identical, but the implementation is much easier to write and read.
function* makeRangeIterator(start = 0, end = Infinity, step = 1) { let iterationCount = 0; for (let i = start; i < end; i += step) { iterationCount++; yield i; } return iterationCount; }
yield | The yield operator is used to pause and resume a generator function. |
---|
Iterables
An object is iterable if it defines its iteration behavior, such as what values are looped over in a for...of construct. Some built-in types, such as Array or Map, have a default iteration behavior, while other types, such as Object do not.
In order to be iterable, an object must implement the @@iterator method. This means that the object (or one of the objects up its prototype chain) must have a property with a Symbol.iterator key.
It may be possible to iterate over an iterable more than once, or only once. It is up to the programmer to know which is the case.
Iterables which can iterate only once, such as Generators customarily return this from their @@iterator method, whereas iterables which can be iterated many times must return a new iterator on each invocation of @@iterator.
function* makeIterator() { yield 1; yield 2; } function runScript2() { let returnStr = ""; let divResult = document.getElementById("results2"); let iterationCount = 0; const it = makeIterator(); for (const itItem of it) { iterationCount++; returnStr += "<br/>Iteration: "+ iterationCount +" - Item: "+ itItem; } divResult.innerHTML = returnStr; }
Run Script 2Generator Response
function* makeIterator3() {
yield getVal3_1();
yield 2;
yield getVal3_2();
yield getVal3_3();
yield 2;
}
function getVal3_1() {return 12;}
function getVal3_2() {return 712;}
function getVal3_3() {return 162;}
function runScript3() {
let returnStr = "";
let divResult = document.getElementById("results3");
let iterationCount = 0;
const it = makeIterator3();
for (const itItem of it) {
iterationCount++;
returnStr += "
Iteration: "+ iterationCount +" - Item: "+ itItem;
}
divResult.innerHTML = returnStr;
}
Generator Response
function runScript4() { let returnStr = ""; let divResult = document.getElementById("results4"); const appleStore = countAppleSales(); // Generator { } returnStr += "<br/>"+ appleStore.next().value; // { value: 3, done: false } returnStr += "<br/>"+ appleStore.next().value; // { value: 3, done: false } returnStr += "<br/>"+ appleStore.next().value; // { value: 3, done: false } returnStr += "<br/>"+ appleStore.next().value; // { value: undefined, done: true } divResult.innerHTML = returnStr; } function* countAppleSales() { const saleList = [3, 7, 5]; for (let i = 0; i < saleList.length; i++) { yield saleList[i]; } }
Run Script 4Generator Response
function runScript5() { let returnStr = ""; let divResult = document.getElementById("results5"); let iterationCount = 0; const appleStore = countAppleSales(); // Generator { } for (const itItem of appleStore) { iterationCount++; returnStr += "<br/>Iteration: "+ iterationCount +" - Item: "+ itItem; } divResult.innerHTML = returnStr; } function* countAppleSales() { const saleList = [3, 7, 5]; for (let i = 0; i < saleList.length; i++) { yield saleList[i]; } }
Run Script 5Generator Response
Run Script 6
Generator Response
Run Script 7