Modern JavaScript: Map Iterators
Welcome to the Map Iterators lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
Maps have
.keysand.valuesmethods. The keys and values come out in their original insertion order. This is nice, and not all languages' map types support this!>
const catAges = new Map([['Ms. Fluff', 4],['Keanu', 2],]);Array.from(catAges.keys());Result:
['Ms. Fluff', 'Keanu']
>
const catAges = new Map([['Keanu', 2],['Ms. Fluff', 4],]);Array.from(catAges.keys());Result:
['Keanu', 'Ms. Fluff']
>
const catAges = new Map([['Keanu', 2],['Ms. Fluff', 4],]);Array.from(catAges.values());Result:
[2, 4]
We can turn a map into an array with
Array.from. The keys and values will come out in the same format that the constructor takes: a sequence of two-element[key, value]arrays.>
const emails = new Map();emails.set('Amir', 'amir@example.com');emails.set('Betty', 'betty@example.com');- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
emails.size;Result:
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
Array.from(emails);Result:
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const [firstUser] = Array.from(emails);firstUser;Result:
['Amir', 'amir@example.com']
We can also destructure a map directly, without converting it to an array first:
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const [, secondUser] = emails;secondUser;Result:
['Betty', 'betty@example.com']
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const [, [name, email]] = emails;`The email for ${name} is ${email}`;Result:
'The email for Betty is betty@example.com'
Maps will also accept iterables as their constructor arguments.
>
function* users() {yield ['Amir', 'amir@example.com'];yield ['Betty', 'betty@example.com'];}const userMap = new Map(users());userMap.get('Betty');Result:
'betty@example.com'
Maps themselves are iterable. That's why we were able to do
Array.from(someMap)in code examples earlier in this lesson: theArray.frommethod uses the iteration protocols to iterate over the map's contents.We can also create a map by giving it an iterable of 2-element arrays holding the keys and values. It's no coincidence that the formats for these are the same: the map itself is an iterable of 2-element arrays, and we can create a new map from any iterable of 2-element arrays. This symmetry lets us copy a map by doing
new Map(someExistingMap).>
const emails = new Map([['Amir', 'amir@example.com']]);const emails2 = new Map(emails);Array.from(emails2);Result:
[['Amir', 'amir@example.com']]
Constructing maps is a good place to revisit iterator laziness. We'll build up an example in two parts. First, we'll define a generator that yields a total of 30,000
[key, value]entries.>
function* manyDuplicates() {for (let i=0; i<10000; i++) {// Yield entries of [number, isEven]yield [1, false];yield [2, true];yield [3, false];}}Array.from(manyDuplicates()).length;Result:
30000
The for loop above runs 10,000 times. Each iteration yields three values, each of which becomes an element in the final array:
[1, false],[2, true], and[3, false], over and over. 10,000 * 3 = 30,000 total elements.When we build a map from these elements, there are only three unique keys:
1,2, and3. The final map will only have those three keys.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const map = new Map(manyDuplicates());Array.from(map);Result:
The
Mapconstructor fetched each of the 30,000 entries from the iterator one at a time. But remember that a generator never builds a full list of its contents. Instead, its iterator gives us one value viayield, then pauses the generator function, waiting for us to request the next value. At any given time, only one of the values actually exists in memory.As a result, the code above consumed almost no memory. It would consume the same amount of memory if there were 300,000 entries, or 3,000,000,000 entries! The only memory needed is: (1) the small amount used by the generator as it's producing individual entries, and (2) the small amount that the map needs to store its three entries.
Our example was only a toy, but this issue comes up in real systems. Suppose that we need to iterate over a billion analytics events stored in a database. That could easily require a terabyte of memory, which our server probably doesn't have!
We can avoid loading all of the records at once by "streaming" them from the disk with a generator or some other type of iterator. As a generous estimate, we might use 100 kilobytes of memory to do that streaming, working with only one record in memory at a time. That's about 0.00001% of the memory that we'd use if we tried to load them all at once!