Maps and Sets in JavaScript

Wednesday, October 14, 2015

JavaScript has never offered a variety of data structures out of the box. If you needed any structure fancier than an array, you'd have to roll your own or borrow some code.

The 2015 specification brings a number of keyed collections to the language, including the Map, Set, WeakMap, and WeakSet.

Maps

Although you can treat any JavaScript object as a dictionary or key-value store, there are some drawbacks. Most notably, the only type of keys you can use to index into an object are keys of type string. The new Map collection removes the restriction on key types and offers some additional conveniences. The core of the API revolves around the get and set methods.

let map = new Map();

// add a new key-value pair
map.set("key", 301);

// overwrite an existing value
map.set("key", 302);

expect(map.get("key")).toBe(302);

The delete and has methods are also handy.

let map = new Map();
map.set("key", 611);

expect(map.has("key")).toBe(true);

map.delete("key");
expect(map.has("key")).toBe(false);

A key can now be any type of object.

var someKey = { firstName: "Scott"};
var someValue = { lastName: "Allen"};

var map = new Map();
map.set(someKey, someValue);

expect(map.size).toBe(1);
expect(map.get(someKey).lastName).toBe("Allen");
expect(map.get({firstName:"Scott"})).toBeUndefined();

When using objects as keys, JavaScript will compare pointers behind the scenes, as you probably expect (and as the above code demonstrates).

You can also construct a Map using a two dimensional array of key-value pairs.

let map = new Map([
    [1, "one"],
    [2, "two"],
    [3, "three"]
]);

Finally, maps are iterable in a number of ways. You can retrieve an iterator for the keys in a map (using the keys method), the values in a map (using the values method), and the key-value entries in a map (using the entries method, as you might have guessed). The map also has the magic [Symbol.iterator] method, so you can use a Map directly in a for of loop.

let map = new Map([
    [1, "one"],
    [2, "two"],
    [3, "three"]
]);
    
let sum = 0;
let combined = "";
for(let pair of map) {
    sum += pair[0];
    combined += pair[1];
} 
    
expect(map.size).toBe(3);
expect(sum).toBe(6);
expect(combined).toBe("onetwothree");

map.clear();
expect(map.size).toBe(0);

Note how each entry comes out as an array with the key in index 0 and the value in index 1. This is a scenario where destructuring is handy. 

for(let [key, value] of map) {
    sum += key;
    combined += value;
}

Set

Just like in mathematics, a Set maintains a collection of distinct objects. The API is slightly different from a Map. For example, you can construct a set by passing in any iterator.

let animals = ["bear", "snake", "elephant", "snake"];
let animalsSet = new Set(animals.values());

expect(animals.length).toBe(4);
expect(animalsSet.size).toBe(3);
expect(animalsSet.has("bear")).toBe(true);

animalsSet.delete("bear");
expect(animalsSet.size).toBe(2);

Just like a Map, a Set compares object types by comparing references. Unlike other languages, there is no ability to provide an alternative comparer strategy.

let scott = { name: "Scott "};

let people = new Set();
people.add(scott);
people.add(scott);
people.add( {name:"Scott"});

expect(people.size).toBe(2);
expect(people.has(scott)).toBe(true);
expect(people.has({name:"Scott"})).toBe(false);

Weak Collections

The WeakMap and WeakSet collections serve a similar purpose to their non-weak counterparts, but the objects held in these collections are eligible for garbage collection at any given point in time, making these weak collections ideal for many extensibility scenarios. The special nature of weak collections means you cannot iterate over the collections, because an entry could disappear before the iteration completes. There are also some other small API differences. For example, the keys for a Map and the values for a Set can only be object types (no primitives like strings), and there is no size method to uncover the number of entries in these collections. 

Coming up soon - more stimulating API additions in 2015 for objects, strings, and arrays.


Comments
gravatar SlurpTheo Wednesday, October 14, 2015
Thanks for these! Typo: Search for 'sting type'.
gravatar Scott Allen Wednesday, October 14, 2015
@Theo Fixed - thanks!
gravatar Michael Thursday, October 15, 2015
Hi Scott, thanks again for a great post. What would be a use case for the weak types? The only possible one I see is an un-managed cache. But is there all that much gain? also how harsh is the automatic collection? Thanks.
gravatar Alex B Friday, October 16, 2015
Thanks! But can I use it with Babel now for everything IE9+ ?
gravatar Stuart Sunday, October 18, 2015
How about an example of using a Map with a non-primitive, like an object literal. What would be the use case for this and how would you specify equality if you needed to?
gravatar scott Monday, October 19, 2015
@Stuart: Take a look at the third code sample. The third sample demonstrates the runtime is comparing references. There is not way to change the check for equality like you can in many other languages.
gravatar scott Monday, October 19, 2015
@Alex: Maps and Sets require a polyfill at runtime to make them work on older browsers. Core-js and the Babel runtime can provide these features.
gravatar scott Monday, October 19, 2015
@Michael: One use case would be eventing. Imagine you want to invoke a method on some object when an interesting event occurs, like when a user becomes authenticated. Holding a reference to the object from a singleton event aggregator means the runtime can never garbage collect the subscribers to the events, even though they may be transient objects. Using weak references allows the GC to remove objects. Here is another example, more concrete. You want to observe any changes on an object without preventing a GC of the object: https://github.com/ericz/polymer-mutationobserver/blob/master/src/MutationObserver/MutationObserver.js
Comments are closed.

My Pluralsight Courses

K.Scott Allen OdeToCode by K. Scott Allen
What JavaScript Developers Should Know About ECMAScript 2015
The Podcast!