Sets
Collection of unique values
In JavaScript sets are not of an explicit data type. JavaScript provides a predefined Set
object.
Sets are collections of unique values (both in JavaScript and in a mathematical context).
The values can be of any data type.
Sets are different from arrays. Storing a set of elements in a JavaSvaript set has some advantages over storing it in an array, as will be explained in the section Arrays vs. sets.
The number of values can be obtained by the size
property, somewhat like the length
property of an array.
const mySet = new Set();
mySet.add('How to get this value?');
mySet.add(true);
mySet.add(Symbol('fizz'));
mySet.add({a: 1});
mySet.add({a: 1});
console.log(mySet); // logs: [ "How to get this value?", true, Symbol("fizz"), {a: 1}, {a: 1} ]
console.log(mySet.size); // logs: 5
PS. In the above example it looks like an array is returned to the console. This is how the Firefox browser logs a set in the console.
Chrome logs a set between curly brackets: { "How to get this value?", true, Symbol("fizz"), {a: 1}, {a: 1} }
.
Each value must be unique in the set's collection.
However, multiple values in a set cannot be NaN
, even though NaN !== NaN
and
one value can be 0
while another is -0
, even though 0 === -0
.
In the above example both values {a: 1}
reference a different object, so they are considered different values.
Constructor
Sets cannot be created with a literal. They are created with the Set
constructor and the new
operator.
Values are inserted into the set by the add()
method.
const mySet = new Set();
const value1 = 'Hello';
const value2 = {};
const value3 = function() {};
const value4 = null;
mySet.add(value1);
mySet.add(value2);
mySet.add(value3);
mySet.add(value4);
console.log(mySet); // logs: Set [ "Hello", {}, value3(), null ]
The Set
constructor takes an optional argument. If this argument is an iterable object,
all of its elements will be added to the new set. The argument could for instance be an array.
This can be used to transform an array into a set, copy sets or merge sets (see later).
const myArray = [1, ["hello", {}], Symbol("foo"), NaN];
const mySet = new Set(myArray);
console.log(mySet); // logs: Set [ 1, [ "hello", {} ], Symbol("foo"), NaN ]
It also provides a way to create and fill a set in a little more compact way than using the Set
constructor
and the add
method.
const mySet = new Set([ 1, true, "hello", {}, null]);
console.log(mySet); // logs: Set [ 1, true, "hello", {}, null ]
Accessing and iterating over values
The set's values are not object properties, like the elements of an array are.
Values are not accessible via the regular dot notation or square bracket notation.
Enumerating properties, for instance by means of a for...in
loop, will not visit the set's values, while it will
visit elements in an array.
Since there is no key attached to a set value, you cannot directly get, say, the second value in the set.
You can use has()
to check if a value is present in the set or not.
This does not work if the value is an object, since the has()
method's argument references a different object than the key ({} !== {}
).
const mySet = new Set([
{a: 1},
[2],
]);
console.log(mySet.has({a: 1})); // logs: false
console.log(mySet.has([2])); // logs: false
The set's elements can be iterated over in insertion order, i.e., the order in which each value was first inserted into the set.
In a for...of
loop the set's built-in iterator
returns, at every iteration, the subsequent value.
Also Set.prototype.forEach()
can be used to iterate over the set's elements.
const mySet = new Set([
"pit",
"pot",
"put"
]);
mySet.add("pot".replace(/o/, "e"));
console.log(mySet.size); // logs: 4
console.log(mySet.has('pet')); // logs: true
mySet.prop = "someValue";
console.log(mySet.prop); // logs: "someValue"
console.log(Object.getOwnPropertyNames(mySet)); // logs: Array [ "prop" ]
console.log(mySet.size); // logs: 4
const values = [];
for (const element of mySet) {
values.push(element);
}
console.log(values); // logs: Array [ "pit", "pot", "put", "pet" ]
values.length = 0;
mySet.forEach(element => values.push(element));
console.log(values); // logs: Array [ "pit", "pot", "put", "pet" ]
Like Array
and Map
also Set
has methods values()
and keys()
.
But since sets do not involve keys, Set.prototype.values()
and Set.prototype.keys()
do exactly the the same:
they return an iterator/iterable that yields the value for each element in the set in insertion order.
Looping over this iterator/iterable by a for...of
loop will yield the same result as for...of
looping over the set itself.
const mySet = new Set([
"pit",
"pot",
"put",
"pet"
]);
const valuesArray = [],
keys = mySet.keys(),
values = mySet.values();
for (const element of keys) {
valuesArray.push(element);
}
console.log(valuesArray); // logs: Array [ "pit", "pot", "put", "pet" ]
valuesArray.length = 0;
for (const element of values) {
valuesArray.push(element);
}
console.log(valuesArray); // logs: Array [ "pit", "pot", "put", "pet" ]
// ***********
for (const element of values) {
console.log(element);
}
// logs nothing; an object that is both an iterator and an iterable
// can only be iterated over once.
BTW. An iterable iterator with Symbol.iterator
returning the iterable itself, can
only be iterated over once.
Deleting set elements
The Set.prototype.delete()
method can be used to delete a value from the set.
The size
property cannot be used to truncate a set, unlike the length
property on an array.
Method clear
can be used to delete all values from the set.
const mySet = new Set([
"Hello",
313,
true,
null
]);
console.log(mySet); // logs: Set [ "Hello", 313, true, null ]
mySet.delete(true);
console.log(mySet.has(true)); //logs: false
mySet.size = 0;
console.log(mySet.size); // logs: 3
mySet.clear();
console.log(mySet.size); // logs: 0
Cloning and merging sets
We have seen that we can use the Set
constructor taking an iterable object as the argument.
We can use this to clone or merge sets. The clones and merges are shallow, meaning the data itself is not cloned.
You can directly use the original set as argument, but you can also use spread syntax in an array as the argument. In the latter case, sets essentially convert to arrays first. This may be a little unwieldy for just cloning a set, but useful to merge sets, as will be explained next.
const originalSet = new Set([ 2, 3, 5, 7 ]);
const cloneSet = new Set(originalSet);
console.log(cloneSet); // logs: Set [ 2, 3, 5, 7 ]
console.log(originalSet === cloneSet); // false
const otherCloneSet = new Set([...originalSet]);
console.log(otherCloneSet); // logs: Set [ 2, 3, 5, 7 ]
Sets can be merged using an array argument for the constructor. This way you can merge any iterable objects to a set. The value uniqueness will be maintained by overwriting a previous value with the last iterated over same value.
const firstSet = new Set([ 2, 3, 5, 7, 11 ]);
const secondSet = new Set([ 11, 13, 17, 19 ]);
const mergedSet = new Set([...firstSet, ...secondSet]);
console.log(mergedSet); // logs: Set [ 2, 3, 5, 7, 11, 13, 17, 19 ]
const myString = "QED"; // the String wrapper object is iterable
const composedSet = new Set([...mergedSet, 23, 29, ...myString]);
console.log(composedSet); // logs: Set [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, "Q", "E", "D" ]
Converting to and from sets
We have seen that we can use the Map
constructor to convert iterable objects to sets.
const myArray = [ 2, 4, 6, 8 ];
const mySet = new Set(myArray);
console.log(mySet); // logs: Set [ 2, 4, 6, 8 ]
We can convert sets into arrays by using Array.from()
or spread syntax.
Note that all conversions are shallow.
const mySet = new Set([ 2, 4, 6, 8 ]);
console.log(Array.from(mySet)); // logs: Array [ 2, 4, 6, 8 ]
console.log([...mySet]); // logs: Array [ 2, 4, 6, 8 ]
Strings can be converted to sets via the iterable String
wrapper object.
Conversion is case sensitive and duplicate characters are omitted.
const myString = "Oompa Loompa";
const mySet = new Set(myString);
console.log(mySet); // logs: Set [ "O", "o", "m", "p", "a", " ", "L" ]
Converting the set back to a string:
const mySet = new Set([ "O", "o", "m", "p", "a", " ", "L" ]);
console.log(Array.from(mySet).join("")); // logs: "Oompa L"
console.log([...mySet].join("")); // logs: "Oompa L"
We can use array-set-array conversion to remove duplicate elements from an array.
const myArray = [ "a", "b", "c", "c", "d", "e", "e", "f", "F" ];
const purgedArray = [...new Set(myArray)];
console.log(purgedArray); // logs: Array [ "a", "b", "c", "d", "e", "f", "F" ]
BTW. Note that the uniqueness of string values in a set is case sensitive.
Mathematical set operations
In mathematics, set theory features binary operations on sets. Some of them are:
-
Union.
The union of two sets is the set containing only and all elements of both sets (no duplicates).
// Merging two sets: function union(setA, setB) { return new Set([...setA, ...setB]); }; console.log(union(new Set([1, 2, 3]), new Set([2, 3, 4, 5]))); // logs: Set [ 1, 2, 3, 4, 5 ]
// A little more cumbersome than using spread syntax: function union(setA, setB) { const _union = new Set(setA); for (const elem of setB) { _union.add(elem); } return _union; }; console.log(union(new Set([1, 2, 3]), new Set([2, 3, 4, 5]))); // logs: Set [ 1, 2, 3, 4, 5 ]
-
Intersection.
The intersection of two sets is the set containing only and all elements belonging to one set and also belong to the other set.
In other words: the set of all shared elements.
function intersection(setA, setB) { const _intersection = new Set(); for (const elem of setB) { if (setA.has(elem)) { _intersection.add(elem); } } return _intersection; }; console.log(intersection(new Set([1, 2, 3]), new Set([2, 3, 4, 5]))); // logs: Set [ 2, 3 ]
-
Set difference.
The difference of set A and set B
is the set of elements in A but not in B.
function difference(setA, setB) { const _difference = new Set(setA); for (const elem of setB) { _difference.delete(elem); } return _difference; }; console.log(difference(new Set([1, 2, 3, 4]), new Set([3, 4, 5]))); // logs: Set [ 1, 2 ]
-
Symmetric difference.
The symmetric difference of two sets is the set of elements which are in either of the sets, but not in their intersection.
In other words: all elements that are not shared.
function symmetricDifference(setA, setB) { const _difference = new Set(setA); for (const elem of setB) { if (_difference.has(elem)) { _difference.delete(elem); } else { _difference.add(elem); } } return _difference; }; console.log(symmetricDifference(new Set([1, 2, 3, 4]), new Set([3, 4, 5]))); // logs: Set [ 1, 2, 5 ]
Set A is a superset of set B if all elements of B are also elements of A. Set B is then a subset of A.
function isSuperset(set, subset) {
for (const elem of subset) {
if (!set.has(elem)) {
return false;
}
}
return true;
};
console.log(
isSuperset(
new Set(["pit", "pet", "pot", "put"]),
new Set(["pet", "pot"])
)
);
// logs: true
Arrays vs. sets
Storing a set of elements in a JavaScript set has some advantages over storing it in an array.
-
Deleting array elements by value (
myArray.splice(myArray.indexOf(value), 1)
) is a lot slower than deleting set elements (mySet.delete(value)
). - Values in a set are automatically guaranteed unique. In an array you would have to manually keep track of duplicates.
-
The value
NaN
cannot be found withindexOf
in an array.