JavaScript Generators

JavaScript arrays and collections are considered container data types: an array is a single-value container storing elements accessed through their indices, a Map instance is an associative container composed of key/value pairs. Implicit iteration through the items of a container is based on standard loop statements. Enhanced techniques may rest upon the use of a generator - a special routine maintaining the state of iteration.

JavaScript generators introduced in ECMAScript 6 look a lot like generator functions implemented in Python: after returning an intermediate result to a caller, the generator keeps its state and is resumed when the next element of a container is requested.

def generatordemo():
    browsers=["Chrome", "Opera", "Firefox"]
    for browser in browsers:
        yield browser

g=generatordemo()
print g.next() # Chrome
print g.next() # Opera
print g.next() # Firefox
print g.next() # raises StopIteration

The browsers is a list - a sequence containing three elements. Each time the next() method of g is called, the generator yields the next element of the list. While producing the values, the generator does not return to the beginning of the sequence: it resumes where it has last left off. When all the values have been yielded, the generator raises a StopIteration exception.

By the way, browsers mentioned in the example above support JavaScript generators, so developers can use any of them to experiment with the new technology. JS generator functions are designated by the asterisk character (the ASCII code is 42):

function * generator() {
 var i=0;
 while(i<4) {
  yield i++; // the increment occurs after the expression is evaluated
 }
}

var g=generator();
var item=g.next();
console.log(item); // Object {value: 0, done: false}
. . .

After the next() method has been invoked, an object with two properties is returned: the value property holds data yielded by the generator, the boolean done reflects its state.

After all valid data items have been sent to the caller, the generator starts producing objects with the value property equal to undefined:

item=g.next(); // the next() method has been called five times
console.log(item); // Object {value: undefined, done: true}

By monitoring the done property the caller code can control the iteration:

var g=generator();
var item;
while((item=g.next()).done===false) {
 console.log(item.value); // the values of 0, 1, 2, 3 are logged out
}

Multiple Yield Expressions

The yield operator can be used in the body of a generator function many times. Yield expressions may deal with values of various types. Here's an example of creating a minimalistic text-to-PDF converter implemented on the client side: the generator produces diverse data items for building a PDF document. The code is based on the PDFKit library:

var textArea=document.querySelector("textarea"); // contents of the <textarea> will be converted to PDF

function * pdfobjects() {
 yield {Title: "PDFKit Example", Author: "Developer", Producer: "PDF-to-Text Converter"};
 yield textArea.value;
 yield {x: 10 , y: 10, align: "justify"};
 yield "application/pdf";
}

The generator has four yield expressions and provides four different data items: an object containing PDF metadata, a text to convert, an object holding values for the text representation, and the MIME type of the new PDF document. Conversion can be triggered by the user action (e.g. by a button click):

var launcher=document.querySelector("button");
launcher.addEventListener("click", convert);

function convert() {
 var iframe=document.createElement("iframe"); // PDF document will be displayed in an <iframe>
 iframe.width=parseInt(getComputedStyle(textArea).width); // adjusting <iframe> dimensions
 iframe.height=parseInt(getComputedStyle(textArea).height);

 var g=pdfobjects(); // generator instance

 var pdf=new PDFDocument();
 var stream=pdf.pipe(new blobStream());
 pdf.info=g.next().value; // PDF metadata
 pdf.text(g.next().value, g.next().value); // a text to convert and options for its rendering
 pdf.end();

 stream.on("finish",function() {
  var blob=stream.toBlob(g.next().value); // PDF MIME type
  iframe.src=URL.createObjectURL(blob);
  document.body.appendChild(iframe);
 });
}

Visual representation of the created document may vary depending on the browser: for example, Mozilla's Firefox will use the PDF.js library to render PDF without external plugins. If the <textarea> had an extract from ECMAScript specification, then the generated PDF in Firefox could look like this:

Generators and Arrays

If a generator has been created to return elements of an array, a loop statement is usually specified in the body of the generator function. However, there's a shorter syntax enabling array iteration. Let's compare two generators:

function * generator() {
 yield [1, 2, 3];
}

function * arraydemo() {
 yield * [1, 2, 3];
}

The first generator will return the array as a whole:

var g=generator();
console.log(g.next()); // Object {value: Array[3], done: false}
console.log(g.next()); // Object {value: undefined, done: true}: no values to yield any more

The second generator yields the arrray elements in sequence:

var a=arraydemo();
console.log(a.next()); // Object {value: 1, done: false}
. . .

Generators and Strings

The same notation based on the use of the asterisk (*) after the yield operator allows us to represent a string as a sequence of characters:

function * generator() { // "one-time" generator
 yield "Hello from generator!";
}

var g=generator();
console.log(g.next()); // Object {value: "Hello from generator!", done: false}
console.log(g.next()); // Object {value: undefined, done: true}: no values to yield any more

function * stringdemo() {
 yield * "Hello from generator!";
}

var s=stringdemo();
console.log(s.next().value); // "H"
console.log(s.next().value); // "e"
console.log(s.next().value); // "l"
console.log(s.next().value); // "l"
console.log(s.next().value); // "o"

Generators and Collections

JavaScript collections are iterable objects exposing their own methods for enumeration. However, the generator is an alternative way of iterating through the elements of a Set or a Map. Similar to examples above, collections in yield expressions should be preceded by the asterisk:

var pl=new Set(["C#", "JavaScript", "Python"]);

function * generator() {
 yield * pl;
}

var g=generator();
console.log(g.next().value+" supports generators."); // "C# supports generators."

Key/value pairs of a Map are yielded as arrays:

var client=new Map();
client.set("browser", navigator);
client.set("capabilities", performance);
client.set("context", window);

function * generator() {
 yield * client;
}

var g=generator();
var item=g.next();;
console.log(item); // Object {value: Array[2], done: false}
console.log(item.value[1].userAgent);

Generators and Object Initializers

An object initializer is a sequence of property/value pairs enclosed in curly braces. A generator routine can be employed in an object initializer like any other ordinary function. To demonstrate the correct binding of this in the body of a generator our example will build an object creating AES keys for symmetric encryption.

First of all, an object initilizer should be specified:

var keyGenerator = {
 secretkey: {}, // Firefox alternative: Object.create(CryptoKey.prototype)
 createSecretKey: generator,
 keyGenParams: {name: "AES-CBC", length: 128},
 isExtractable: true,
 keyUsage: ["encrypt", "decrypt"],
};

The keyGenerator object has three properties presenting parameters for AES keys generation:

  • the keyGenParams object is an instance of AesKeyGenParams: it contains the name of the algorithm (AES-CBC) and the length of the AES key (128 bits);
  • making keys extractable enables the application to export the raw keying material later;
  • the array of key usage values indicates cryptographic operations allowed for generated keys.

The createSecretKey method is a generator. Three properties described above will be used in this statements within the scope of the method. The secretKey property will hold a created cryptographic key:


function * generator() { // generator with infinite loop
 while(true) {
  var promise=crypto.subtle.generateKey(this.keyGenParams, this.isExtractable, this.keyUsage);
  yield promise.then(success);
 }
}

The success function is called when the key-generation promise has been fulfilled. To represent the AES key as a readable sequence, the exportKey() method creates another Promise and tries to convert the opaque key material to the JSON Web Key format:

function success(key) { // promise is fulfilled: the key is generated successfully
 keyGenerator.secretKey=key; // an instance of Key/CryptoKey
 crypto.subtle.exportKey("jwk", key).then(displayKey);
}

function displayKey(jsonwk) { // promise is fulfilled: the key is converted to JSON WK format
 console.log(JSON.stringify(jsonwk)); // logging out the stringified JSON
}

var g=keyGenerator.createSecretKey(); // generator instance
g.next();
g.next();
. . .

Each time the next() method of the generator is called, a new AES key is created and logged out as a stringified JSON structure:

{"alg": "A128CBC", "ext": true, "k": "1wZhts_5JdFK2SlPrx8fBA", "key_ops": ["encrypt","decrypt"], "kty": "oct"}

Generators and JavaScript Exceptions

Error handling in generators can be improved with the help of the throw() method. The generator code below yields a typed array filled with random values. The caller code iterates through the elements of the array and divides 100 by each of them. When 0 is detected, a custom exception is sent to the generator: the throw() method passes a brief error message to the generator. The received string value is handled in the block of catch statements.

function * generator() {
 try {
  var randomValues=new Uint8Array(100);
  crypto.getRandomValues(randomValues);
  yield * randomValues;
 }
 catch(e) {
  console.log("Generator has been notified about an error . . .");
  console.error(e); // error message: 0 is detected!
 }
};

var g=generator();
var item;
while((item=g.next()).done===false) {
 if(item.value===0) {
  g.throw("0 is detected!"); // custom exception
 }
 else {
  console.log(100/item.value);
 }
}

Sending Values to Generators

Sending values to generators is not reduced to error handling only: the next() method can pass any data to the generator function which, in its turn, handles received data items and presents them to the caller:

var value=0;
function * generator() {
 while(true) {
  value=yield ++value;
 }
};

var g=new generator();
console.log(g.next()); // Object {value: 1, done: false}
console.log(value); // 1

console.log(g.next(5)); // Object {value: 6, done: false}
console.log(value); // 6

console.log(g.next(8)); // Object {value: 9, done: false}
console.log(value); // 9

console.log(g.next(20)); // Object {value: 21, done: false}
console.log(value); // 21
. . .

Shorthand Syntax for Generator Definitions

ECMAScript 6 has introduced a shorter syntax for method definitions on objects initializers: the syntax is applied both to ordinary and generator functions. The asterisk (*) in the shorthand version of the code is placed before the name of the method:

var object = { // object initializer
 launcher: function * () { // method as a generator function
  yield * [0, 1, 2];
 }
};

var g=object.launcher();
console.log(g.next()); // Object {value: 0, done: false}


var brief = {
 * launcher() { // generator defined using the shorthand syntax
  yield * [0, 1, 2];
 }
};

var b=brief.launcher();
console.log(b.next()); // Object {value: 0, done: false}

Computed property names - just another proposal of ECMAScript 6 - are fully supported in shorter generator definitions:

var object = {
 * ["launch"+"er"] () {
  yield * [0, 1, 2];
 }
};

var g=object.launcher();


The generator is a worthwhile improvement in JavaScript: by providing advanced iteration techniques, it allocates less memory for complex data structures and makes the code base of an application more compact. Gradual implementation of the new technology in all browsers are inevitable: the use of generators in Python and other languages has convincingly proved the efficiency and practical value of generator functions.