Maps and Sets - Learning JavaScript (2016)

Learning JavaScript (2016)

Chapter 10. Maps and Sets

ES6 introduces two welcome data structures: maps and sets. Maps are similar to objects in that they can map a key to a value, and sets are similar to arrays except that duplicates are not allowed.

Maps

Prior to ES6, when you needed to map keys to values, you would turn to an object, because objects allow you to map string keys to object values of any type. However, using objects for this purpose has many drawbacks:

§ The prototypal nature of objects means that there could be mappings that you didn’t intend.

§ There’s no easy way to know how many mappings there are in an object.

§ Keys must be strings or symbols, preventing you from mapping objects to values.

§ Objects do not guarantee any order to their properties.

The Map object addresses these deficiencies, and is a superior choice for mapping keys to values (even if the keys are strings). For example, imagine you have user objects you wish to map to roles:

const u1 = { name: 'Cynthia' };

const u2 = { name: 'Jackson' };

const u3 = { name: 'Olive' };

const u4 = { name: 'James' };

You would start by creating a map:

const userRoles = new Map();

Then you can use the map to assign users to roles by using its set() method:

userRoles.set(u1, 'User');

userRoles.set(u2, 'User');

userRoles.set(u3, 'Admin');

// poor James...we don't assign him a role

The set() method is also chainable, which can save some typing:

userRoles

.set(u1, 'User')

.set(u2, 'User')

.set(u3, 'Admin');

You can also pass an array of arrays to the constructor:

const userRoles = new Map([

[u1, 'User'],

[u2, 'User'],

[u3, 'Admin'],

]);

Now if we want to determine what role u2 has, you can use the get() method:

userRoles.get(u2); // "User"

If you call get on a key that isn’t in the map, it will return undefined. Also, you can use the has() method to determine if a map contains a given key:

userRoles.has(u1); // true

userRoles.get(u1); // "User"

userRoles.has(u4); // false

userRoles.get(u4); // undefined

If you call set() on a key that’s already in the map, its value will be replaced:

userRoles.get(u1); // 'User'

userRoles.set(u1, 'Admin');

userRoles.get(u1); // 'Admin'

The size property will return the number of entries in the map:

userRoles.size; // 3

Use the keys() method to get the keys in a map, values() to return the values, and entries() to get the entries as arrays where the first element is the key and the second is the value. All of these methods return an iterable object, which can be iterated over by a for...of loop:

for(let u of userRoles.keys())

console.log(u.name);

for(let r of userRoles.values())

console.log(r);

for(let ur of userRoles.entries())

console.log(`${ur[0].name}: ${ur[1]}`);

// note that we can use destructuring to make

// this iteration even more natural:

for(let [u, r] of userRoles.entries())

console.log(`${u.name}: ${r}`);

// the entries() method is the default iterator for

// a Map, so you can shorten the previous example to:

for(let [u, r] of userRoles)

console.log(`${u.name}: ${r}`);

If you need an array (instead of an iterable object), you can use the spread operator:

[...userRoles.values()]; // [ "User", "User", "Admin" ]

To delete a single entry from a map, use the delete() method:

userRoles.delete(u2);

userRoles.size; // 2

Lastly, if you want to remove all entries from a map, you can do so with the clear() method:

userRoles.clear();

userRoles.size; // 0

Weak Maps

A WeakMap is identical to Map except:

§ Its keys must be objects.

§ Keys in a WeakMap can be garbage-collected.

§ A WeakMap cannot be iterated or cleared.

Normally, JavaScript will keep an object in memory as long as there is a reference to it somewhere. For example, if you have an object that is a key in a Map, JavaScript will keep that object in memory as long as the Map is in existence. Not so with a WeakMap. Because of this, a WeakMapcan’t be iterated (there’s too much danger of the iteration exposing an object that’s in the process of being garbage-collected).

Thanks to these properties of WeakMap, it can be used to store private keys in object instances:

const SecretHolder = (function() {

const secrets = new WeakMap();

return class {

setSecret(secret) {

secrets.set(this, secret);

}

getSecret() {

return secrets.get(this);

}

}

})();

Here we’ve put our WeakMap inside an IIFE, along with a class that uses it. Outside the IIFE, we get a class that we call SecretHolder whose instances can store secrets. We can only set a secret through the setSecret method, and only get the secret through the getSecret method:

const a = new SecretHolder();

const b = new SecretHolder();

a.setSecret('secret A');

b.setSecret('secret B');

a.getSecret(); // "secret A"

b.getSecret(); // "secret B"

We could have used a regular Map, but then the secrets we tell instances of SecretHolder could never be garbage-collected!

Sets

A set is a collection of data where duplicates are not allowed (consistent with sets in mathematics). Using our previous example, we may want to be able to assign a user to multiple roles. For example, all users might have the "User" role, but administrators have both the "User" and"Admin" role. However, it makes no logical sense for a user to have the same role multiple times. A set is the ideal data structure for this case.

First, create a Set instance:

const roles = new Set();

Now if we want to add a user role, we can do so with the add() method:

roles.add("User"); // Set [ "User" ]

To make this user an administrator, call add() again:

roles.add("Admin"); // Set [ "User", "Admin" ]

Like Map, Set has a size property:

roles.size; // 2

Here’s the beauty of sets: we don’t have to check to see if something is already in the set before we add it. If we add something that’s already in the set, nothing happens:

roles.add("User"); // Set [ "User", "Admin" ]

roles.size; // 2

To remove a role, we simply call delete(), which returns true if the role was in the set and false otherwise:

roles.delete("Admin"); // true

roles; // Set [ "User" ]

roles.delete("Admin"); // false

Weak Sets

Weak sets can only contain objects, and the objects they contain may be garbage-collected. As with WeakMap, the values in a WeakSet can’t be iterated, making weak sets very rare; there aren’t many use cases for them. As a matter of fact, the only use for weak sets is determining whether or not a given object is in a set or not.

For example, Santa Claus might have a WeakSet called naughty so he can determine who to deliver the coal to:

const naughty = new WeakSet();

const children = [

{ name: "Suzy" },

{ name: "Derek" },

];

naughty.add(children[1]);

for(let child of children) {

if(naughty.has(child))

console.log(`Coal for ${child.name}!`);

else

console.log(`Presents for ${child.name}!`);

}

Breaking the Object Habit

If you’re an experienced JavaScript programmer who’s new to ES6, chances are objects are your go-to choice for mapping. And no doubt you’ve learned all of the tricks to avoiding the pitfalls of objects as maps. But now you have real maps, and you should use them! Likewise, you’re probably accustomed to using objects with boolean values as sets, and you don’t have to do that anymore, either. When you find yourself creating an object, stop and ask yourself, “Am I using this object only to create a map?” If the answer is “Yes,” consider using a Map instead.