Built-in Iterators

Iterators are an important part of ECMAScript 6, and as such, you don’t need to create your own iterators for many built-in types; the language includes them by default. You only need to create iterators when the built-in iterators don’t serve your purpose, which will most frequently be when defining your own objects or classes. Otherwise, you can rely on built-in iterators to do your work. Perhaps the most common iterators to use are those that work on collections.

Collection Iterators

ECMAScript 6 has three types of collection objects: arrays, maps, and sets. All three have the following built-in iterators to help you navigate their content:

  • entries() - Returns an iterator whose values are a key-value pair
  • values() - Returns an iterator whose values are the values of the collection
  • keys() - Returns an iterator whose values are the keys contained in the collection

You can retrieve an iterator for a collection by calling one of these methods.

The entries() Iterator

The entries() iterator returns a two-item array each time next() is called. The two-item array represents the key and value for each item in the collection. For arrays, the first item is the numeric index; for sets, the first item is also the value (since values double as keys in sets); for maps, the first item is the key.

Here are some examples that use this iterator:

  1. let colors = [ "red", "green", "blue" ];
  2. let tracking = new Set([1234, 5678, 9012]);
  3. let data = new Map();
  4. data.set("title", "Understanding ECMAScript 6");
  5. data.set("format", "ebook");
  6. for (let entry of colors.entries()) {
  7. console.log(entry);
  8. }
  9. for (let entry of tracking.entries()) {
  10. console.log(entry);
  11. }
  12. for (let entry of data.entries()) {
  13. console.log(entry);
  14. }

The console.log() calls give the following output:

  1. [0, "red"]
  2. [1, "green"]
  3. [2, "blue"]
  4. [1234, 1234]
  5. [5678, 5678]
  6. [9012, 9012]
  7. ["title", "Understanding ECMAScript 6"]
  8. ["format", "ebook"]

This code uses the entries() method on each type of collection to retrieve an iterator, and it uses for-of loops to iterate the items. The console output shows how the keys and values are returned in pairs for each object.

The values() Iterator

The values() iterator simply returns values as they are stored in the collection. For example:

  1. let colors = [ "red", "green", "blue" ];
  2. let tracking = new Set([1234, 5678, 9012]);
  3. let data = new Map();
  4. data.set("title", "Understanding ECMAScript 6");
  5. data.set("format", "ebook");
  6. for (let value of colors.values()) {
  7. console.log(value);
  8. }
  9. for (let value of tracking.values()) {
  10. console.log(value);
  11. }
  12. for (let value of data.values()) {
  13. console.log(value);
  14. }

This code outputs the following:

  1. "red"
  2. "green"
  3. "blue"
  4. 1234
  5. 5678
  6. 9012
  7. "Understanding ECMAScript 6"
  8. "ebook"

Calling the values() iterator, as in this example, returns the exact data contained in each collection without any information about that data’s location in the collection.

The keys() Iterator

The keys() iterator returns each key present in a collection. For arrays, it only returns numeric keys, never other own properties of the array. For sets, the keys are the same as the values, and so keys() and values() return the same iterator. For maps, the keys() iterator returns each unique key. Here’s an example that demonstrates all three:

  1. let colors = [ "red", "green", "blue" ];
  2. let tracking = new Set([1234, 5678, 9012]);
  3. let data = new Map();
  4. data.set("title", "Understanding ECMAScript 6");
  5. data.set("format", "ebook");
  6. for (let key of colors.keys()) {
  7. console.log(key);
  8. }
  9. for (let key of tracking.keys()) {
  10. console.log(key);
  11. }
  12. for (let key of data.keys()) {
  13. console.log(key);
  14. }

This example outputs the following:

  1. 0
  2. 1
  3. 2
  4. 1234
  5. 5678
  6. 9012
  7. "title"
  8. "format"

The keys() iterator fetches each key in colors, tracking, and data, and those keys are printed from inside the three for-of loops. For the array object, only numeric indices are printed, which would still happen even if you added named properties to the array. This is different from the way the for-in loop works with arrays, because the for-in loop iterates over properties rather than just the numeric indices.

Default Iterators for Collection Types

Each collection type also has a default iterator that is used by for-of whenever an iterator isn’t explicitly specified. The values() method is the default iterator for arrays and sets, while the entries() method is the default iterator for maps. These defaults make using collection objects in for-of loops a little easier. For instance, consider this example:

  1. let colors = [ "red", "green", "blue" ];
  2. let tracking = new Set([1234, 5678, 9012]);
  3. let data = new Map();
  4. data.set("title", "Understanding ECMAScript 6");
  5. data.set("format", "print");
  6. // same as using colors.values()
  7. for (let value of colors) {
  8. console.log(value);
  9. }
  10. // same as using tracking.values()
  11. for (let num of tracking) {
  12. console.log(num);
  13. }
  14. // same as using data.entries()
  15. for (let entry of data) {
  16. console.log(entry);
  17. }

No iterator is specified, so the default iterator functions will be used. The default iterators for arrays, sets, and maps are designed to reflect how these objects are initialized, so this code outputs the following:

  1. "red"
  2. "green"
  3. "blue"
  4. 1234
  5. 5678
  6. 9012
  7. ["title", "Understanding ECMAScript 6"]
  8. ["format", "print"]

Arrays and sets return their values by default, while maps return the same array format that can be passed into the Map constructor. Weak sets and weak maps, on the other hand, do not have built-in iterators. Managing weak references means there’s no way to know exactly how many values are in these collections, which also means there’s no way to iterate over them.

A> ### Destructuring and for-of Loops A> A> The behavior of the default iterator for maps is also helpful when used in for-of loops with destructuring, as in this example: A> A> js A> let data = new Map(); A> A> data.set("title", "Understanding ECMAScript 6"); A> data.set("format", "ebook"); A> A> // same as using data.entries() A> for (let [key, value] of data) { A> console.log(key + "=" + value); A> } A> A> A> The for-of loop in this code uses a destructured array to assign key and value for each entry in the map. In this way, you can easily work with keys and values at the same time without needing to access a two-item array or going back to the map to fetch either the key or the value. Using a destructured array for maps makes the for-of loop equally useful for maps as it is for sets and arrays.

String Iterators

JavaScript strings have slowly become more like arrays since ECMAScript 5 was released. For example, ECMAScript 5 formalized bracket notation for accessing characters in strings (that is, using text[0] to get the first character, and so on). But bracket notation works on code units rather than characters, so it cannot be used to access surrogate pair characters correctly, as this example demonstrates:

  1. var message = "A 𠮷 B";
  2. for (let i=0; i < message.length; i++) {
  3. console.log(message[i]);
  4. }

This code uses bracket notation and the length property to iterate over and print a string containing a surrogate pair character. The output is a bit unexpected:

  1. A
  2. (blank)
  3. (blank)
  4. (blank)
  5. (blank)
  6. B

Since the surrogate pair character is treated as two separate code units, there are four empty lines between A and B in the output.

Fortunately, ECMAScript 6 aims to fully support Unicode (see Chapter 2), and the default string iterator is an attempt to solve the string iteration problem. As such, the default iterator for strings works on characters rather than code units. Changing this example to use the default string iterator with a for-of loop results in more appropriate output. Here’s the tweaked code:

  1. var message = "A 𠮷 B";
  2. for (let c of message) {
  3. console.log(c);
  4. }

This outputs the following:

  1. A
  2. (blank)
  3. 𠮷
  4. (blank)
  5. B

This result is more in line with what you’d expect when working with characters: the loop successfully prints the surrogate pair character, as well as all the rest.

NodeList Iterators

The Document Object Model (DOM) has a NodeList type that represents a collection of elements in a document. For those who write JavaScript to run in web browsers, understanding the difference between NodeList objects and arrays has always been a bit difficult. Both NodeList objects and arrays use the length property to indicate the number of items, and both use bracket notation to access individual items. Internally, however, a NodeList and an array behave quite differently, which has led to a lot of confusion.

With the addition of default iterators in ECMAScript 6, the DOM definition of NodeList (included in the HTML specification rather than ECMAScript 6 itself) includes a default iterator that behaves in the same manner as the array default iterator. That means you can use NodeList in a for-of loop or any other place that uses an object’s default iterator. For example:

  1. var divs = document.getElementsByTagName("div");
  2. for (let div of divs) {
  3. console.log(div.id);
  4. }

This code calls getElementsByTagName() to retrieve a NodeList that represents all of the <div> elements in the document object. The for-of loop then iterates over each element and outputs the element IDs, effectively making the code the same as it would be for a standard array.