barbarian meets coding

WebDev, UX & a Pinch of Fantasy

A Look at ES6 Sets

| Comments

The Mastering the Arcane Art of JavaScript-mancy series are my humble attempt at bringing my love for JavaScript to all other C# developers that haven’t yet discovered how awesome this language and its whole ecosystem are. These articles are excerpts of the super duper awesome JavaScript-Mancy book a compendium of all things JavaScript for C# developers.

A Set is a data structure that represents a distinct collection of items in which each item is unique and only appears once. If you have been working with JavaScript for a little while chances are that you have need to roll your own implementation of a Set. Well, you’ll need to do that no longer beause ES6 comes with a native Set implementation.

Working With Sets

You can experiment with all examples in this article directly within this jsBin.

You can create a new set using the Set constructor:

1
let set = new Set();

Or you can create a set from an iterable collection like an array:

1
2
3
4
let elementsOfMagic = new Set(['earth', 'fire', 'air', 'earth', 'fire', 'water']);

console.log(`These are the elements of magic: ${[...elementsOfMagic]}`);
// => These are the elements of magic: earth, fire, air, water

As you can appreciate from the example above, a Set will automatically remove the duplicated items and only store each specific item once. You can easily add more items to a Set using the add method:

1
2
3
4
elementsOfMagic.add('aether');

console.log(`More magic!: ${[...elementsOfMagic]}`);
// => More magic!: earth, fire, air, water, aether

The add method is chainable, so adding multiple new items is very convenient:

1
elementsOfMagic.add('earth').add('air').add('water');

You can check whether an item exists within a Set by using the has method:

1
2
console.log(`Is water one of the sacred elements of magic? ${elementsOfMagic.has('water')}`)
// => Is water one of the sacred elements of magic? true

And you can remove items from a set using the delete method:

1
2
3
4
5
6
7
8
9
elementsOfMagic.delete('aether');

console.log(`The aether element flows like the tides and
like the tides sometimes disappears:
${[...elementsOfMagic]}`);

// => The aether element flows 
//    like the tides and sometimes disappears: 
//    earth,fire,air,water

Additionally, you can get the number of elements within a set using the size property:

1
console.log(`${elementsOfMagic.size} are the elements of magic`);

And remove all the items from a set using the clear method:

1
2
3
4
5
const castMagicShield = () => elementsOfMagic.clear();
castMagicShield();

console.log(`ups! I can't feel the elements: ${elementsOfMagic.size}`);
// => ups! I can't feel the elements: 0

If you take a minute to reflect about the Set API and try to remember the Map from the previous article you’ll realize that both APIs are exceptionally consistent with each other. Consistency is awesome, it will help you learn these APIs in a glimpse and result in a less error-prone code. Let’s see how we iterate over the elements of a Set next.

Iterating Sets

Just like Map you can iterate over the elements of a Set using the for/of loop:

1
2
3
4
5
6
7
8
elementsOfMagic.add('fire').add('water').add('air').add('earth');
for(let element of elementsOfMagic){
  console.log(`element: ${element}`);
}
// => element: fire
//    element: water
//    element: air
//    element: earth

In this case, instead of key/value pairs you iterate over each item within a Set. Notice how the elements are interated in the same order as they were inserted. The default iterator for a Set is the values iterator. The next snippet of code is equivalent to the one above:

1
2
3
for(let element of elementsOfMagic.values()){
  console.log(`element: ${element}`);
}

The Set also has iterators for keys and entries just like the Map although you probably won’t need to use them. The keys iterator let’s you iterate over the same collection of items within the Set (so it’s equivalent to values). The entries iterator transforms each item into a key/value pair where both the key and the value are each item in the Set. So if you use the entries iterator you’ll just iterate over [value, value] pairs.

In addition to using either of these iterators, you can take advantage of the Set.prototype.forEach method to traverse the items in a Set:

1
2
3
4
5
6
7
elementsOfMagic.forEach((value, alsoValue, set) => {
  console.log(`element: ${value}`);
})
// => element: fire
//    element: water
//    element: air
//    element: earth

Using Array Methods With Sets

The conversion between Sets to Arrays and back is so straightforward that using all the great methods available in the Array.prototype object is one little step away:

1
2
3
4
5
6
7
8
function filterSet(set, predicate){
    var filteredItems = [...set].filter(predicate);
    return new Set(filteredItems);
}

var aElements = filterSet(elementsOfMagic, e => e.startsWith('a'));
console.log(`Elements of Magic starting with a: ${[...aElements]}`);
// => Elements of Magic starting with a: air

We saw many of these methods in the Array’s article and you can find many more in this other article on JavaScript and LINQ.

How Do Sets Understand Equality?

So far you’ve seen that a Set removes duplicated items whenever we try to add them to the Set. But how does it know whether or not two items are equal?

Well… It uses strict equality comparison to determine that (which you may also know as === or !==). This is important to understand because it sets a very big limitation to using Sets in real world applications today. That’s because even though strict equality comparison works great with numbers and strings, it compares objects by reference, that is, two objects are only equal if they are the same object.

Let’s illustrate this problematic situation with an example. Let’s say that we have a Set of persons which of course are unique entities (we are all beautiful wonders like precious stones):

1
let persons = new Set();

We create a person object randalf and we attempt to add it twice to the Set:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let randalf = {id: 1, name: 'randalf'};

persons
  .add(randalf)
  .add(randalf);

console.log(`I have ${persons.size} person`)
// => I have 1 person 

console.log([...persons]);
// => [[object Object] {
//  id: 1,
//  name: "randalf"
//}]

The Set has our back and only adds the person once. Since it is the same object, using strict equality works in this scenario. However, what would happen if we were to add an object that we considered to be equal in our problem domain? And let’s say that in our current example two persons are equal if they have the same properties, and particularly the same id (because I am sure there’s many randalfs around, although I’ve never met any of them):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
persons.add({id: 1, name: 'randalf'});
console.log(`I have ${person.size} persons?!?`)

// => I have 2 persons?!?
console.log([...persons]);
/*
*= [[object Object] {
  id: 1,
  name: "randalf"
}, [object Object] {
  id: 1,
  name: "randalf"
}]
*/

Well, in that case, the object would be added to the Set and as a result, and for all intents and purposes, we would have the same person twice. Unfortunately there’s no way to specify equality for the elements within a Set as of today and we’ll have to wait to see this feature introduced into the language some time in the future.

We are free to imagine how it would look though, and something like this would work wonderfully:

1
let personsSet = new Set([], p => p.id);

In the meantime, if you need to use Set-like functionality for objects your best bet is to use a dictionary indexing objects by a key that represents their uniqueness.

1
2
3
4
5
6
var fakeSetThisIsAHack = new Map();
fakeSetThisIsAHack
  .set(randalf.id, randalf)
  .set(1, {id: 1, name: 'randalf'});
console.log(`fake set has ${fakeSetThisIsAHack.size} item`);
// => fake set has 1 item

Concluding

Sets is one of the new data structures in ES6 that lets you easily remove duplicates from a collection of items. It offers a very simple API very consistent with the Map API and it’s going to be a great addition to your arsenal and save you the need to roll your own Set implementation in your programs. Unfortunately, at present, it has a big limitation that is that it only supports strict equality comparison to determine whether two items are equal. Hopefuly in the near future we will be able to define our own domain specific ways to define equality and that day Sets will achieve their true potential. Until then use Set with numbers and strings, and rely on Map when you are working with objects.

Buy the Book!

Are you a C# Developer interested in learning JavaScript? Then take a look at the JavaScript-mancy book, a complete compendium of JavaScript for C# developers.

a JavaScriptmancy sample cover

Have a great week ahead!! :)

More Articles in These Series

Comments