Eslam Hefnawy Serverless architect at Serverless, Inc. Co-creator of the Serverless Framework and the lead architect of Serverless Components.

ES6 keyed collections: Maps and sets

5 min read 1534

ES6 Keyed Collections: Maps and Sets

JavaScript keyed collections are structured collections of data that store values and provide methods for easily accessing them. Sounds a lot like arrays and objects, right?

Keyed collections were actually introduced with ES6 as an alternative to arrays and objects, which were the only data structures available in JS to that point. Although they were good enough for storing data, objects and arrays had some limitations and were a bit painful to work with.

For example, to iterate over an object or to sort it, you had to first convert it to an array, then perform those operations. With arrays, looping was not a problem, but if you needed to pull out a specific value without its index, it was not at all straightforward.

Keyed collections — namely, Map, Set, WeakMap, and WeakSet — were introduced to solve these issues and to make working with values or key-value pairs easier.

In this guide, we’ll take a closer look at maps and sets and examine their syntax, how they differ from objects and arrays, and what methods they offer. We’ll also compare their performance.

JavaScript sets

JavaScript sets resemble arrays in the sense that they are also collections of values. But, unlike arrays, these data collections can only include unique values. In other words, you can’t have duplicates in a set.

The second difference between arrays and sets is that inside sets, the values are stored in no particular order, so you can just call them by their name.

Let’s create a new set to see it in action. You can find the examples used in this first exercise on JSFiddle.

let events = new Set();
let event1 = { type: "concert", day: "Saturday" };
let event2 = { type: "book launch", day: "Wednesday"};
let event3 = { type: "conference", day: "Thursday"};
let event4 = { type: "meet up", day: "Monday" };
// Let's add each event to the set
events.add(event1);
events.add(event2);
events.add(event3);
events.add(event4);

As you can see, the syntax is very simple. You create a new set with the new Set() method and use the add() method to push the values in the set.

To see what values the set contains, you can use the values() method inside a for … of loop.

We made a custom demo for .
No really. Click here to check it out.

for (let item of events.values()) {
  console.log(item);
}

If you want to check whether a specific value is found inside the set, you can use the has(value) method. To delete an item, you can use the delete(value) method.

console.log(events.has(event2));

events.delete(event3);
for (let value of events) {
        console.log(value)
}

Other useful methods available for sets include clear() and size(). The former removes all the items from the set, while the latter returns the number of elements in the set.

We’ve established that this type of collection can only contain unique values. What happens if we want to add an event twice?

let cities = new Set();

let city1 = { name: "London" };
let city2 = { name: "Barcelona"};
let city3 = { name: "Milan"};

cities.add(city1);
cities.add(city2);
cities.add(city1);
cities.add(city3);

cities.forEach((city, cities) => {
  console.log(city);
});

This will list the names of the three cities, each of them only once.

As you can see, the syntax and the methods of sets are very simple and easy to use. But when and why would you use this type of keyed collection?

Set vs. array: Uses and performance

Converting sets to arrays and vice versa is easy to do and very handy if you want to perform operations such as filtering and returning the unique values from a data collection.

Here’s how to turn a set into an array:

let set = new Set([9, 15, "a string", {"objectKey": "objectValue"}]);
set.add(true);

let arr = [...set]; // destructuring

console.log(arr); fj

// Outputs [9, 15, "a string", {objectKey: "objectValue"}, true]

As you can see, the set contains a combination of data types this time: numbers, a string, an object, and a boolean. To convert this set to an array, we used restructuring.

Here’s how to convert an array to a set:

let arr2 = [9, 15, "a string", {"objectKey": "objectValue"}];

let arr2converted = [...new Set(arr2)];

console.log(arr2converted);

// Outputs [9, 15, "a string", {objectKey: "objectValue"}, true]

Again, you can find the code for this exercise on JDFiddle.

Now let’s look at an example where we have duplicate items in an array and we want to filter them out. We can do this in two ways:

// Method 1

let users = ["John", "Murray", "Jane", "Jane", "Anne"];

function unique(users) {
        return Array.from(new Set(users));
}

console.log(unique(users));

// Method 2

let set = new Set(users);
let arrFromSet = [...set];

console.log(arrFromSet);

The code for this exercise is available on JSFiddle.

Finally, let’s say we want to add all the users from above to a new set and a new array. Let’s see which collection performs the operation faster.

let arr = [], set = new Set();
let users = ["John", "Murray", "Jane", "Jane", "Anne", "John", "Murray", "Jane", "Jane", "Anne"];

for (let i = 0; i < users.length; i++) {
  arr.push(users[i]);
  set.add(users[i]);
}

let result;

console.time('Array'); 
result = arr.indexOf("Anne") !== -1; 
console.timeEnd('Array');

console.time('Set'); 
result = set.has("Anne"); 
console.timeEnd('Set');

Run this code directly in your console. Here are the results:

Array: 0.013916015625ms
Set: 0.0078125ms

The difference is very small here, but the set is faster. If you perform such operations on big data sets, the latter collection type is a better choice.

JavaScript maps

Maps can be used instead of objects in situations where you need to use a key-value pair but want a bit more flexibility. As the name implies, they’re just used to map a key to a value.

In JavaScript objects, each key in the pair needs to be either a string or a symbol. In maps, however, the keys are unrestricted, meaning you can use another object, a function, or even a primitive type as the key.

Here’s what the map syntax looks like:

let users = [{
    id: 1,
    name: 'John'
  },
  {
    id: 2,
    name: 'Murray'
  },
  {
    id: 3,
    name: 'Jane'
  },
  {
    id: 4,
    name: 'Jane'
  },
  {
    id: 5,
    name: 'Anne'
  }
]

let userNames = users.map(function(user) {
  console.log(user.name)
});

Without this type of keyed collection, you would have to first create an empty array into which you would then push all user names.

let userNms = [];

users.forEach(function (user) {
  userNms.push(user.name);
});

console.log(userNms);

Here’s the code for this exercise.

Maps use methods similar to those used by sets: clear, delete, has, values, entries, forEach. We won’t cover them all, but we’ll look at three methods that are specific to maps: set(), get(), and entries().

Set() adds a key-value pair to the Map object, while get() retrieves the value for the specified key.

Here’s an example:

const user1 = new Map();
user1.set('id', 1);
user1.set('name', 'John');

console.log(user1.get('id'));

What if we want to get the key-value pairs from the map collection? We can use the entries() method with an iterator.

const user1 = new Map();
user1.set('id', 1);
user1.set('name', 'John');

console.log(user1.get('id'));

let iterator = user1.entries();

console.log(iterator.next().value);
console.log(iterator.next().value);

Here’s the code.

Map vs. object: Uses and performance

Maps and objects are very much alike, but the main difference is that inside a map, any data type can be a key, so you’re not limited to strings. This is extremely useful when you want to store object-related data but don’t want to add it to the object itself or use an array of objects due to their limitations.

You can also directly iterate over the keys or values of a map. With objects, you would first have to convert them to an array, which isn’t always practical. Below is an example of iteration over a map collection.

let userIDs = new Map();

let user1 = {name: 'John'}, user2 = {name: 'Murray'}, user3 = {name: 'Jane'};

userIDs.set(user1, 1) .set(user2, 2) .set(user3, 3);

// Method 1

for (let [name, id] of userIDs) {
  console.log(name);
  console.log(id);
}

// Method 2

userIDs.forEach((name, id) => {
  console.log(name);
  console.log(id);
});

Here’s the code for this example.

To convert an object to a map, we can use the Object.entries() method.

const obj = {
  'name': John,
  'id': 1,
}

const map = new Map(Object.entries(obj));

console.log(map.get('name')) 
// Outputs John

Now, let’s compare an object to a map and see how they do in terms of performance. We’ll use the same example as before, when we compared sets with arrays.

let obj = {}, map = new Map();

let users = ["John", "Murray", "Jane", "Jane", "Anne", "John", "Murray", "Jane", "Jane", "Anne"];

for (let i = 0; i < users.length; i++) {
  obj[i] = i;
  map.set(i, i);
}

let result;

console.time('Object'); 
result = obj.hasOwnProperty("Anne"); 
console.timeEnd('Object');

console.time('Map'); 
result = map.has("Anne"); 
console.timeEnd('Map');

You can find the code for this exercise here.

The performance of these two collections is as follows.

Object: 0.031982421875ms
Map: 0.0146484375ms

For comparison, the performance for array and set was:

Array: 0.013916015625ms
Set: 0.0078125ms

As you can see, although maps and sets are similar to arrays and objects, these newer keyed collections are more flexible, easier to iterate over, and higher-performing.

: Debug JavaScript errors easier by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

.
Eslam Hefnawy Serverless architect at Serverless, Inc. Co-creator of the Serverless Framework and the lead architect of Serverless Components.

Leave a Reply