Diving into Sets

by kirupa   |   filed under JavaScript 101

When it comes to storing a collection of data, arrays probably come to mind first. They’ve been around forever, are very flexible, and contain a boatload of properties that make using them a breeze. Over the past few years, JavaScript gained another way to store a collection of data. That way is via this mysterious array-looking creature known as a Set. On the surface, arrays and sets look similar:

But there are a bunch of characteristics that make them different. The biggest characteristic is whether duplicate values are allowed to be stored or not. With a set, we can only store unique items. This means we can use a set to store whatever we want (like an array), but we can store that item only once (unlike an array). If we try to add a duplicate of an item that is already a part of our set, that item is ignored and not added to our collection. Nifty, right?

In the following sections, we’ll go into more detail on what sets are and how to use them like a professional ninja!

Onwards!

Creating a Set, Part I

Before we can use a set, we need to first create it. There isn’t a whole lot of drama here. The only way we can create a set is by calling on the Set constructor:

let mySet = new Set();

When this code runs, we will have created an empty Set object called mySet:

Now, you may be wondering if there are other cleverer ways to create sets outside of typing in new Set() like an animal. The answer is a Nope.

Adding Items to a Set

Once we have a set, we can add items to it by using the add method:

let mySet = new Set();
mySet.add("blarg");
mySet.add(10);
mySet.add(true);

Now, here is where the uniqueness enforcement superpowers ⚡ of sets comes into play. Right now, our set contains the text value blarg, the number 10, and the boolean true. If we try to add a new item that already exists in our set, nothing new will get added. Take a look at the following highlighted line:

let mySet = new Set();
mySet.add("blarg");
mySet.add(10);
mySet.add(true);
mySet.add("blarg") // rut roh

We are trying to add the text blarg one more time to our set. The blarg item already exists, so our set won’t add this duplicated item one more time:

Reacting to duplicate elements without making a fuss is one of the Set’s strongest differentiators compared to other data structures like arrays. When our set encounters a duplicate item, it just ignores it and the rest of our code executes as if nothing out of the ordinary happened.

How Checking for Duplicates Works

For every item we add, our Set object has a really fast way of checking whether the item we are adding is equal to another item already in the set. The way our Set will check for equality with another item is by using the strict equality (aka ===) approach. This is an important detail to call out, for it may be the source of some frustration if we aren’t careful. By relying on ===, what our set is checking for is equality of primitive values and object references. The primitive value part is what we have been seeing so far in our code where we added some text, a number, and a boolean. Something like the following doesn’t have any surprises:

let sayWhat = new Set();
sayWhat.add("Lobby!");
sayWhat.add("Lobby!");
sayWhat.add("Lobby!");
sayWhat.add("Lobby!");
sayWhat.add("Lobby!");
sayWhat.add("Lobby!");
sayWhat.add("Lobby!");
sayWhat.add("Lobby!");
sayWhat.add("Lobby!");

console.log(sayWhat); // Lobby!

Now, here is where things get a little bit interesting. Take a look at the following example:

let anotherSet = new Set();
anotherSet.add(true);
anotherSet.add("abc");
anotherSet.add([1, 2]);
anotherSet.add([1, 2]);

What do you think the contents of our anotherSet object will be? The answer is true"abc"[1, 2], and [1, 2]. The part that might seem trippy is the two [1, 2] arrays that we are adding. To us human beings, both of those arrays seem the same. They are representing what looks to be identical things. To the === check our set performs, those two arrays are distinct. What our set will declare as equal is when object references refer to the same thing. The following snippet highlights this:

let myArray = [1, 2];
let anotherSet = new Set();

anotherSet.add(true);
anotherSet.add("abc");
anotherSet.add(myArray);
anotherSet.add(myArray);

In this case, we have our myArray object that stores our array values of 1 and 2. It is this object we are now adding twice to our set, and since we are adding two myArray object references, the === operator will say that they are both the same. The end result will be that our array will end up getting represented inside our set just once. The contents of anotherSet in this situation will be true"abc", and [1, 2].

Creating a Set, Part 2

Earlier, we saw how to create an empty set that we then added items to. There is another way we can create sets. It still involves the new keyword, but we can pass in an existing collection of data when creating our set to pre-populate it:

let someValues = ["a", "b", "c", 10, "a", "c", false];
let newSet = new Set(someValues);
console.log(newSet); // "a", "b", "c", 10, false

In this snippet, we have our someValues array that contains a handful of items, and some of the items like the a and the c are duplicated. When creating our newSet object, we still use the new Set() expression, but we pass in the someValues array to our Set constructor. When our set gets created this time, it isn’t empty. It contains the unique values from the items we passed in when creating our set. Our duplicate items get filtered out.

This might bring up another question. What sorts of item collections can we pass in to the Set constructor when creating a set? The answer is any iterable object. An iterable object is just a fancy name for any object that provides a way for us to cycle through all of its values. An array is one example of such an object. Text (Strings), TypedArrays, Maps, other Set objects, NodeList, and a handful more fall into the iterable object bucket. There are few really technical things an object must also satisfy to be considered iterable, and you can read more about that in this excellent MDN article on this subject.

Before we wrap this section up, take a look at the following where we pass in a string (aka an iterable object!) as part of creating our set:

let textSet = new Set("diplodocus");
console.log(textSet); // d, i, p, l, o, c, u, s

We pass in the word diplodocus, and what gets stored by our set are the unique characters from it. Notice that each letter ends up becoming an individual entry in our set. Whenever an iterable object is passed in, each individual value from that object is evaluated for uniqueness and added to our set if that value is indeed unique.

Very Relevant Tip

Did you know that a Diplodocus is the longest type of dinosaur we’ve discovered so far? Yeah...share that in your next standup!

Checking the Size of Our Set

To figure out how many items live inside our set, we have access to the handy size property:

let setCount = new Set();
console.log(setCount.size); // 0

setCount.add("foo");
console.log(setCount.size); // 1

setCount.add("bar");
setCount.add("zorb");
console.log(setCount.size); // 3

The value returned by the size property gets updated each time we add or remove (see next section) items from our set.

Deleting Items from a Set

To delete or remove an item from a set, we can use the appropriately named delete method and pass in the value of the item we are looking to remove:

var robotSounds = new Set(["beep", "boop", "who dis?"]);
robotSounds.delete("who dis?");

console.log(robotSounds) // "beep", "boop"

When you delete an item, the deleted item is both removed from the set and a value of true is returned:

let robotSounds = new Set(["beep", "boop", "who dis?"]);

if (robotSounds.delete("who dis?")) {
  console.log("Item successfully deleted!");
}

console.log(robotSounds) // "beep", "boop"

If we attempt to delete an item that doesn’t exist, our set remains unchanged and false is returned by our delete method instead.

While deleting items individually is handy, there may be times when we just want to fully empty all items from our set. We can do that by using the clear method:

let vegetables = new Set(["🥑","🌶️", "🥦", "🥓", "🥔"]);
console.log(vegetables.size); // 5

vegetables.clear();
console.log(vegetables.size); // 0

Another way to clear all the items from the set is by doing a new Set() to re-create our Set object. It turns out that it isn’t actually faster, so we should just stick with the clear method for efficiently emptying all items from our set.

Checking if an Item Exists

Not only is a set really fast at checking for duplicates, it is also really fast at checking if an item exists in its collection in the first place. To check whether an item exists, we can use the has method:

let ingredients = new Set(["milk", "eggs", "cheese", "tofu"]);

if (ingredients.has("tofu")) {
  ingredients.delete("tofu");
  ingredients.add("bacon");
}

console.log(ingredients); // "milk", "eggs", "cheese", "bacon"

The has method takes the item we want to check for as its argument. If the item is found, it returns a true. If the item doesn’t exist in the collection, it returns a false. The way the check works, as we saw earlier as part of identifying duplicates, is by testing for strict equality (===).

Looping Through Items in a Set

There will be times when we’ll need to loop through the items in a set. The way we can do this is by using the for...of looping pattern. Take a look at the following example:

let textSet = new Set("diplodocus");

for (let letter of textSet) {
  console.log(letter);
}

This for loop will run until every item in the set has been reached. The order the items from our set will be accessed is the same as the order they were added to the set in the first place. Unlike arrays, sets don’t have any concept of index positions that we can loop through. We have to use this for...of approach.

Entries, Keys, and Values

Under the covers, sets store items in the form of key and value pairs. This is something that makes the most sense when visualized. Let’s say we have the following code:

let animaniacs = new Set(["Yakko", "Wakko", "Dot"]);

Inside our animaniacs set, the items YakkoWakko, and Dot will look a bit as follows:

Think of the internals of our set being like a database or a spreadsheet with two columns. One column is labeled Key. Another column is labeled Value. Each row represents the item we are trying to store. The thing that makes sets a bit interesting when compared to other key/value storage arrangements (like a hashtable for example) is that both keys and values store the same data. That is why in our example, YakkoWacko, and Dot appear in both the key column as well as the value column. All of this is a bit strange, but...as the kids say these days, whatevs!

The reason why we spent this time looking at this key and value malarkey is that the Set object provides us with a handful of methods that return all the keys, values, and actual key/value pairs (called entries) that make up a set. Take a look at the following snippet:

let animaniacs = new Set(["Yakko", "Wakko", "Dot"]);

console.log(animaniacs.keys());
console.log(animaniacs.values());
console.log(animaniacs.entries());

The names of these methods should help clarify what type of data they will return. The keys method returns all the keys, the values method returns all of the values, and the entries method returns the key/value pair for each item in our set. The way the data is returned is not in the form of something like an array or generic object. The data is returned in the form of an Iterator object. This means the way you can access the items is by using the similar for...of approach we saw earlier:

for (let item of animaniacs.keys()) {
  console.log(item); // "Yakko", "Wakko", "Dot"
}

Iterators are really neat and provide a lot of cool functionality for iterating to items, so take a look at this article on Iterators and Generators.

Conclusion

If you read through every section, you learned almost everything there is to know about the Set object and the various properties and methods that you'll likely end up using. What makes sets really useful is their lightning-fast way of detecting duplicate items and ensuring only unique values are stored by them. You'll find yourself relying on sets more and more for a variety of simple and not-so-simple data-related tasks. We'll cover some of the more common data-related tasks in a little bit.

If you have a question about this or any other topic, the easiest thing is to comment below or drop by our forums where a bunch of the friendliest people you'll ever run into will be happy to help you out!

THE KIRUPA NEWSLETTER

Get cool tips, tricks, selfies, and more...personally hand-delivered to your inbox!

( View past issues for an idea of what you've been missing out on all this time! )

WHAT'S ON YOUR MIND

HOT FORUM TOPICS

Serving you freshly baked content since 1998!

Killer hosting by (mt) mediatemple

Facebook Twitter Youtube Pinterest Instagram Github
BACK TO TOP
new books - yay!!!