Execute Program

Modern JavaScript: Maps

Welcome to the Maps lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • A JavaScript object's property keys can be strings, numbers, or symbols.

  • >
    const aString = 'aString';
    const aNumber = 5;
    const aSymbol = Symbol('aSymbol');
    const obj = {
    [aString]: 1,
    [aNumber]: 2,
    [aSymbol]: 3,
    };
    [obj[aString], obj[aNumber], obj[aSymbol]];
    Result:
    [1, 2, 3]Pass Icon
  • If we try to use any other value as a key, that value is automatically converted into a string.

  • >
    const obj = {
    [undefined]: 1,
    };
    Object.keys(obj);
    Result:
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    obj['undefined'];
    Result:
    1Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    obj[undefined];
    Result:
    1Pass Icon
  • Note that the last two examples look different, but give identical results! The undefined value becomes a string when used as an object key. When we write obj[undefined], it's as if we'd written obj['undefined'].

  • Similar problems happen when we try to use arrays, objects, or null as property keys. JavaScript coerces those values into strings, leading to potentially confusing bugs.

  • >
    const msFluff = {
    name: 'Ms. Fluff'
    };
    const catCoats = {};

    /* Property keys must be strings. We're using the `msFluff` object as a
    * key in another object, so it gets converted into a string.
    * Unfortunately, that string is '[object Object]'. */
    catCoats[msFluff] = 'calico';

    Object.keys(catCoats);
    Result:
  • Fortunately, JavaScript gives us a way to address this limitation. The Map data type maps keys to values, like objects do. But a map's keys can be anything: arrays, objects, functions, other maps, etc.

  • We start by creating an empty map.

  • >
    const userEmails = new Map();
  • We use .set(key, value) to assign a value to a key. Then, we can use .get(key) to retrieve the value at that key.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    userEmails.set('Amir', 'amir@example.com');
    userEmails.get('Amir');
    Result:
    'amir@example.com'Pass Icon
  • Unlike with objects and arrays, we can't do someMap[someKey]. JavaScript will happily return a value (probably undefined) but it won't be what we want!

  • We can give a map some initial values by passing an array of two-element arrays. Each two-element array specifies a key and a value.

  • >
    const userEmails = new Map([
    ['Amir', 'amir@example.com'],
    ['Betty', 'betty@example.com']
    ]);
    userEmails.get('Betty');
    Result:
    'betty@example.com'Pass Icon
  • Regular JavaScript objects can only have one value at a given key. When we give a key a new value, it overwrites the old value.

  • >
    const emails = {};
    emails['Betty'] = 'betty.j@example.com';
    emails['Betty'] = 'betty.k@example.com';
    emails['Betty'];
    Result:
    'betty.k@example.com'Pass Icon
  • Maps work in the same way: .setting a key overwrites any current value at that key.

  • >
    const emails = new Map();
    emails.set('Betty', 'betty.j@example.com');
    emails.set('Betty', 'betty.k@example.com');
    emails.get('Betty');
    Result:
    'betty.k@example.com'Pass Icon
  • We can use .has to check for whether a certain key exists on a map, and we can .delete any key.

  • >
    const emails = new Map([
    ['Amir', 'amir@example.com'],
    ['Betty', 'betty@example.com']
    ]);
    emails.delete('Amir');
    [emails.has('Amir'), emails.has('Betty')];
    Result:
    [false, true]Pass Icon
  • We can also use .clear to remove every key in the map.

  • >
    const emails = new Map();
    emails.set('Cindy', 'cindy@example.com');
    emails.clear();
    emails.set('Dalili', 'dalili@example.com');
    [emails.has('Cindy'), emails.has('Dalili')];
    Result:
    [false, true]Pass Icon
  • When we try to .get a key that doesn't exist, maps respond like objects do: they return undefined.

  • >
    const emails = {
    Amir: 'amir@example.com'
    };
    emails['Betty'];
    Result:
    undefinedPass Icon
  • >
    const emails = new Map();
    emails.set('Amir', 'amir@example.com');
    emails.get('Betty');
    Result:
    undefinedPass Icon
  • Maps have a .size property that returns the number of items (the number of keys) in the map.

  • >
    const emails = new Map();
    emails.set('Betty', 'betty.j@example.com');
    emails.size;
    Result:
    1Pass Icon
  • By contrast, plain objects don't have a built-in way to retrieve their size. To get the count of keys in an object, we have to do more work, like Object.keys(someObject).length.

  • >
    const emails = {
    'Betty': 'betty.j@example.com',
    };
    Object.keys(emails).length;
    Result:
    1Pass Icon
  • Maps' sizes respect the fact that each key can only exist once. When we set a new value for an existing key, the number of keys doesn't change, so the map's size also doesn't change.

  • >
    const emails = new Map();
    emails.set('Betty', 'betty.j@example.com');
    emails.set('Betty', 'betty.k@example.com');
    emails.size;
    Result:
    1Pass Icon
  • A map's keys don't have to be strings. They can be anything: arrays, objects, or even null and undefined.

  • Now let's see what maps are good for. Imagine that we're building a social network where users can follow each other. We can use a map to remember each user's followers. Each user's username becomes a key in the map. The value at that key is an array of other users who follow the first user.

  • (We could also use the user objects themselves as the map keys, mapping each user object to an array of their followers. That works just fine with maps, but isn't possible with a regular object. But to keep the code short, we'll work with usernames instead of full user objects.)

  • For example, Amir and Cindy might both follow Betty:

  • >
    const follows = new Map();
    follows.set('betty', ['amir', 'cindy']);
  • To decide whether Cindy follows Betty, we .get Betty from the map. That gives us an array of Betty's followers. Then we can check for whether the array .includes Cindy.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    follows.get('betty').includes('cindy');
    Result:
    truePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    follows.get('betty').includes('dalili');
    Result:
    falsePass Icon
  • To add a follower, we .get the first user's follower array, then .push the follower onto it. When we .get the user's follower array again later, we'll see that newly-added follower.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    follows.get('betty').push('dalili');
    follows.get('betty').includes('dalili');
    Result:
    truePass Icon
  • What if a user tries to follow someone that doesn't have any followers yet? For example, what if Betty decides to follow Dalili back? We'll get an error, since the 'dalili' key doesn't exist in the map, giving us undefined back rather than an array. (You can type error when a code example will throw an error.)

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    follows.get('dalili').push('betty');
    Result:
    TypeError: Cannot read properties of undefined (reading 'push')Pass Icon
  • We can guard against this by checking for whether the follows map .has an entry for this user. If there's no entry, we .set an empty array for that user. We use .set here because we're not modifying an existing array; we're inserting a new array where nothing existed before.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    if (!follows.has('dalili')) {
    follows.set('dalili', []);
    }
    follows.get('dalili').push('betty');
    follows.get('dalili').includes('betty');
    Result:
    truePass Icon
  • We now know how to check a user's followers, and how to add followers. We can use this knowledge to model a whole social network!

  • Here's a code problem:

    Write a SocialGraph class with a constructor and two methods.

    1. The constructor creates an empty Map.
    2. The addFollow method records that follower follows followed by updating the map. Note that order matters here! (If Amir follows Betty, that doesn't mean that Betty follows Amir.)
    3. The follows method checks for whether follower follows followed, returning a boolean.

    These notes may help:

    1. You can use the array's includes method to check for whether a user is in another user's array of followers.
    2. Sometimes the follows method is called with a followed user who has no followers, so they won't have an entry in the map yet. You'll need to handle that case and return false.
    class SocialGraph {
    constructor() {
    this.map = new Map();
    }

    addFollow(follower, followed) {
    if (!this.map.has(followed)) {
    this.map.set(followed, []);
    }
    /* We store one array per followed user. It's also possible to solve this
    * problem "backwards", by storing one array per following user. Either
    * type of solution works. */
    this.map.get(followed).push(follower);
    }

    follows(follower, followed) {
    if (!this.map.has(followed)) {
    return false;
    } else {
    return this.map.get(followed).includes(follower);
    }
    }
    }
    const socialGraph = new SocialGraph();
    socialGraph.addFollow('amir', 'betty');
    socialGraph.addFollow('amir', 'cindy');
    socialGraph.addFollow('betty', 'cindy');

    [
    socialGraph.follows('amir', 'betty'),
    socialGraph.follows('amir', 'cindy'),
    socialGraph.follows('betty', 'amir'),
    socialGraph.follows('betty', 'cindy'),
    socialGraph.follows('cindy', 'amir'),
    socialGraph.follows('cindy', 'betty'),
    ];
    Goal:
    [true, true, false, true, false, false]
    Yours:
    [true, true, false, true, false, false]Pass Icon
  • Some final notes about terminology. The Map data type stores a mapping from keys to values. The .map method on arrays builds a new array where each element is replaced with ("mapped to") a new value:

  • >
    [1, 2, 3].map(n => 2 * n);
    Result:
  • The data type Map and the method .map are related at the conceptual level: they both "map" (or "relate") things to other things. Other than that, they have no relationship.

  • Terminology for maps varies by language. Most languages don't distinguish between maps and what JavaScript calls "objects". Instead, they have a single data type similar to JavaScript's maps.

  • This data type is called "map" in JavaScript and Clojure; "dictionary" in Python and C#; and "hash" in Perl and Ruby. Fortunately, these data types all work in a similar way regardless of their names!