A look at ES6 Sets
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:
let set = new Set()
Or you can create a set from an iterable collection like an array:
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:
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:
elementsOfMagic
.add('earth')
.add('air')
.add('water')
You can check whether an item exists within a Set
by using the has
method:
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:
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:
console.log(`${elementsOfMagic.size} are the elements of magic`)
And remove all the items from a set using the clear
method:
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:
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:
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
:
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:
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):
let persons = new Set()
We create a person object randalf
and we attempt to add it twice to the Set
:
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):
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:
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.
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.
Have a great week ahead!! :)
More Articles In This Series
- Chapter 0: The Basic Ingredients of JavaScriptMancy
- Chapter 1: The Many a One JavaScript Quirks
- Functions
- Object Oriented Programming
- Basics
- ES6
- OOP
- Introduction to OOP in JavaScript for C# Developers
- Summoning Fundamentals: Introduction to OOP In JavaScript – Encapsulation
- Summoning Fundamentals: Introduction to OOP In JavaScript – Inheritance
- Summoning Fundamentals: Introduction to OOP In JavaScript – Polymorphism
- White Tower Summoning: Mimicking C# Classical Inheritance in Javascript
- White Tower Summoning Enhanced: The Marvels of ES6 Classes
- Black Tower Summoning: Object Composition with Mixins
- Safer JavaScript Composition With Traits and Traits.js
- JavaScript Ultra Flexible Object Oriented Programming with Stamps
- Object Oriented JavaScript for C# Programmers
- Tooling
- Data Structures
- Functional Programming
- And more stuff to be shaped out:
- Functional Programming with JavaScript
- Modules
- Asynchronous Programming
- The JavaScript Ecosystem
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.