JavaScript maps and sets part 3: sets

JavaScript maps and sets part 3: sets

11 Jan 2021 weak-sets sets javascript

Welcome to the third and final part of my three-part guide to JavaScript maps and sets. In this part, we'll be looking at sets and weak sets - sort of like arrays, but different.

Sets: unique arrays 🔗

Sets are to arrays what maps are to objects. They store values, in a precise order (the order of insertion), and are iterable. The key difference that values must be unique.

If you try to insert a duplicate value into a set, it won't be added a second time. Values are therefore subject to "value equality" checks when you insert, using the same algorithm behind the === (value and type) algorithm.

This wasn't always the case; before ECMAScript 2015, a different, and more inconsistently implemented, algorithm was used, as MDN notes.

let set = new Set(), obj = {foo: 'bar'}; set.add(5) set.add(5); set.add(obj); set.add(obj); set.size; //2, not 4

Sets are great for unique collections because JavaScript doesn't support the concept of uniquity in arrays in the way that, say, PHP does.

$arr = [1, 1, 1, 5, 5]; $unique = array_unique($arr); //[1, 5]

Sets and keys 🔗

One interesting thing about sets is that, unlike arrays, the concept of keys is de-emphasised.

This becomes immediately apparent when we try to add a value to a set and then retrieve its value in the normal array-style way:

let set = new Set(); set.add(5); set[0]; //undefined

Instead, we need to derive an iterator and get the value that way.

set.values().next().value; //get the first item [...set][0]; //likewise

Hazy on iterables/iterators? Check out my detailed guide.

With sets, the emphasis is much more on values than keys and indices. Say we wanted to delete an item from a set. We can use the delete() method, which takes as its only argument the value to remove, not the key.

let set = new Set(); set.add(5); set.delete(5); set.size; //0

Contrast that with arrays, where we a) have to delete based on key; and b) have to use the clunky splice(), an archaic method which, counter-intuitively, can be used to delete or add items to an array, or both.

let arr = [1, 3, 5], deleteIndex = arr.indexOf(3); //1, i.e. position 1 arr.splice(deleteIndex, 1); //delete 1 item from start position 1 console.log(arr); //[1, 5]

Much easier with sets. We pass the value we want to remove, and since each value can exist in the set only once, we know it will only ever result in a single, specific value being removed.

let set = new Set(); set.add('foo'); set.add('bar'); set.delete('foo'); [...set]; //["bar"]

Sets are iterable 🔗

Like the arrays they're related to, sets are iterable. This means we can feed a set to some kind of iterator construct:

let set = new Set([1, 3]); for (let val of set) console.log(val); //1, 3 console.log(...set); //spread syntax - same result

This invokes the default iterator, which for sets is its values() method. That is, it's this method that a set's Symbol.iterator property points to. The following, then, are equivalent:

for (let value of set) console.log(value); //uses default iterator - value() for (let value of set.values()) console.log(value); //explicit use of entries() for (let value of set[Symbol.iterator]()) console.log(value); //alias of values()

API 🔗

We've already seen much of the set API above, but let's look at it in its entirety here.

Constructor 🔗

First, there's the constructor, Set(iterable). You can pass an optional iterable (e.g. an array) to pre-populate it.

let set = new Set([1, 2, 3]); [...set.values()]; //[1, 2, 3]

.size 🔗

Like maps, sets support a size property to return their current length.

let set = new Set(3, 3, 7); set.size; //2

Set content methods 🔗

Next up there's a few methods used to handle the content of a set. These are:

  • add(value) - add a value to the set - returns the set, so you can chain multiple calls to add()
  • clear() - empty the set
  • delete(value) - remove a value from the array by value
  • has(value) - returns boolean denoting whether the set contains a the passed value

Iteration methods 🔗

Finally there's a few methods to derive iterators, namely values(), forEach() and entries().

These work just like their map API counterparts. One interesting point regards, entries(), though. With sets, each entry is made up of two, identical parts - the value.

let set = new Set(); set.add('foo'); for (let entry of set.entries()) console.log(entry); //["foo", "foo"]

This is purely in the interests of harmonisation with the map API, i.e. so that entries are always two part - for maps, the key and the value; for sets, the value, twice, becuase sets don't have keys.

For the same reason, while sets do have a keys() iteration method, it returns values, just like values(), not keys.

As noted earlier, values() is the default iterator.

Weak sets 🔗

If you're familiar with weak maps, this section is going to seem very familiar. I cover weak maps in part 2 of this guide and suggest reading that first, as I talk about the garbage collection implications of "weak" collections more there.

Object values 🔗

Weak sets are to sets what weak maps are to maps. With weak maps, the keys must be objects; with weak sets, the values must be objects.

let wset = new WeakSet(), obj = {foo: 'bar'}; wset.add(obj); //fine wset.add('foo'); //not fine

That last line will throw an error:

Uncaught TypeError: WeakSet value must be an object, got "foo"

Weakly referenced values 🔗

Just like keys in weak maps, objects stored in weak maps are weakly referenced. That is, they do not prevent garbage collection.

I briefly discuss the nature of JavaScript garbage collection in part 2.

This means that, whenever an object in a weak set has no remaining references to it outside of the set, it will be automatically earmarked for garbage collection and, by implication, removed from the set.

let wset = new WeakSet(), obj = {foo: 'bar'}; wset.add(obj); obj = null; //obj will be garbage-collected and removed from the set!

Non-enumerability 🔗

Like weak maps, weak sets are non-enumerable - and for the same reason; namely that, since their contents may be garbage-collected or earmarked for garbage collection at any time, the contents cannot be guaranteed.

Instead, to retrieve a value from a weak set you need a reference to it. You can't browse the contents of weak sets via iteration methods such as values(); these methods aren't available to weak sets as they are to sets.

wset.values(); //undefined wset.entries() //ditto

API 🔗

The weak set API is a stripped-down version of the set API, i.e. minus the iteration methods and the size property. All methods like their counterparts in the regular set API.

  • WeakSet(iterable)
  • add(value)
  • delete(value)
  • has(value)

Summary 🔗

Sets are like arrays, but with unique values. Attempts to insert duplicate values into a set will be ignored. On insertion, values are equated using the === algorithm.

Sets de-emphasise keys. It's not possible to retrieve a value from a set via normal, array-like square-bracket syntax; instead, an iterator must first be derived to retrieve a value.

Weak sets are sets but where values must be obects. Like keys in weak maps, object values in weak sets do not prevent garbage collection. Also like weak maps, weak sets are non-enumerable.

---

So there you have it! I hope you've found this guide to JavaScript maps and sets helpful.