Execute Program

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 .keys and .values methods. 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']Pass Icon
  • >
    const catAges = new Map([
    ['Keanu', 2],
    ['Ms. Fluff', 4],
    ]);
    Array.from(catAges.keys());
    Result:
    ['Keanu', 'Ms. Fluff']Pass Icon
  • >
    const catAges = new Map([
    ['Keanu', 2],
    ['Ms. Fluff', 4],
    ]);
    Array.from(catAges.values());
    Result:
    [2, 4]Pass Icon
  • 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']Pass Icon
  • 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']Pass Icon
  • 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'Pass Icon
  • 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'Pass Icon
  • Maps themselves are iterable. That's why we were able to do Array.from(someMap) in code examples earlier in this lesson: the Array.from method 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']]Pass Icon
  • 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:
    30000Pass Icon
  • 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, and 3. 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 Map constructor 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 via yield, 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!