In this article, we’ve taken a look at Sets and Maps and how they handle unique items and key-value pairs respectively. These useful data structures provide easier and more efficient ways to structure and access data under certain use cases.
Let’s take~ ~a look into two new constructs that were introduced in the JavaScript ES6 specification:
The objective of these new constructs is to provide easier and more efficient ways to structure and access data under certain use cases. In this article, we will look at how Sets and Maps work and explore some of the operations that can be performed on them.
The MDN docs describe the Set object as:
A collections of values. You can iterate through the elements of a set in insertion order. A value in the Set may only occur once; it is unique in the Sets collection.
The JavaScript Set object behaves similarly to the mathematical Set. It allows the addition of distinct values and provides useful methods on its prototype. These methods include - addition, removal and looping over of the items present in the Set.
An array, like a set, is a data structure that allows addition, removal and looping operations on its items. However, an array differs from a set in the sense that it permits the addition of duplicate values and its operations are relatively slower.
Searching through an array has a linear time complexity of O(n), the same as inserting an element in the middle of an array. This means that the running time for searching and inserting items in an array grows as the size of the array increases.
JavaScript’s Push and Pop array methods have a run-time of O(1) which means that: these operations will have a constant time of execution regardless of the size of the array size. However, in practice, the Push operation is O(n) as copy costs are incurred when new contiguous memory locations are allocated to the newly formed array.
In contrast, all insert, delete and search operations for Sets have a running time of just O(1).
Let’s create a Set:
const set = new Set();
console.log(set); // Set {}
To initialize a set, we can pass an array of values to the Set constructor, this will create a Set with those values:
const confectioneries = new Set(['oreo', 'marshmallow','oreo', 'kitkat', 'gingerbread']);
console.log(confectioneries); // result: Set { 'oreo', 'marshmallow', 'kitkat', 'gingerbread' }
In the snippet above, the duplicate value “oreo” is quietly removed from the Set and only unique values are returned.
We can add more items to a Set using the add()
method. This method adds a new value to the Set object and returns the Set. An attempt to add a duplicate item to the Set object wouldn’t return an error, instead, the item will not be added.
Let’s go over an example:
const confectioneries = new Set(['oreo', 'marshmallow', 'kitkat', 'oreo','gingerbread']);
confectioneries.add('donut');
console.log(confectioneries); //_ log result: Set { 'oreo', 'marshmallow', 'kitkat', 'gingerbread', 'donut' } _
confectioneries.add('kitkat');
console.log(confectioneries); //_ log result: Set { 'oreo', 'marshmallow', 'kitkat', 'gingerbread', 'donut' } _
With sets, we can delete items using either of these commands:
delete()
clear()
To use the delete()
method, the value to be deleted is passed to the method. The method will return a Boolean value true
if the deletion was successful and false
if otherwise. We can delete all the elements of the Set object using the clear()
method.
Let’s try out both methods in this example:
confectioneries.delete('kitkat');
console.log(confectioneries); //_ log result: Set { 'oreo', 'marshmallow', 'gingerbread', 'donut' }_
confectioneries.clear();
console.log(confectioneries); // log result: Set {}
We can get the size of a Set using the size
property on the Set prototype. This is similar to the length
property for Arrays:
const confectioneries = new Set(['oreo', 'marshmallow', 'kitkat', 'oreo','gingerbread']);
console.log(confectioneries.size); // log result: 5
We may need to know if a Set has a particular item. This can be accomplished using the has()
method. The has()
method returns true
if the item is in the Set object, and false
if it isn’t:
console.log(confectioneries.has('marshmallow')); // log result: true
We can return the items in a Set object in the same insertion order using the values()
method. This method returns a new setIterator
object . A similar method for returning the items of a set is the keys()
method:
console.log(confectioneries.values()); // _log result: _[_Set Iterator] { 'oreo', 'marshmallow', 'kitkat', 'gingerbread', 'donut' }_
console.log(confectioneries.keys()); //_ log result: _[_Set Iterator] { 'oreo', 'marshmallow', 'kitkat', 'gingerbread', 'donut' }_
The setIterator
object is an Iterator object because it implements the Iteratable and Iterator protocols. The Iterable
protocol specifies a way to iterate through a set of values using loop constructs. It also makes it possible for the values to be iterated using the next()
method. When we call next()
on a setIterator
object, we get the next value in the iteration and a false
until all values of the Set have been iterated over:
let iterator = confectioneries.values();
console.log( iterator.next()); // _{ value: 'oreo', done: false }
_
console.log( iterator.next()); // _{ value: 'marshmallow', done: false }
_
console.log( iterator.next()); //_ { value: 'kitkat', done: false }
_
console.log( iterator.next()); //_ { value: 'gingerbread', done: false }
_
console.log( iterator.next()); //_ { value: 'donut', done: false }
_
console.log( iterator.next()); // _{ value: undefined, done: true }_
Since Sets implement the Iterable protocol, loop constructs such as for ...of
can be used as shown below:
for (let confectionery of confectioneries) {
console.log(confectionery);
}
/_ _console.log() result
oreo
marshmallow
kitkat
gingerbread
donut
__/
WeakSets provide extra flexibility when working with the Set data structure. They are different from regular Sets in that they only accept objects and are not iterable; they can’t be looped over, and do not have a clear()
method. How then do they provide extra flexibility? We’ll see in a bit.
We can create a WeakSet
using the WeakSet
constructor:
let user1 = {name: 'user 1', email: 'user1@example.com'};
let user2 = {name: 'user 2', email: 'user2@example.com'};
let user3 = {name: 'user 3', email: 'user3@example.com'};
const users = new WeakSet([user1, user2, user3]);
The code above creates a new WeakSet
object, adding items other than objects returns a TypeError:
users.add('user 4');
console.log(users); // TypeError: Invalid value used in weak set
Since WeakSets do not have a clear()
method, objects can only be deleted by setting them to null
. This works because the JavaScript Engine’s garbage collection algorithms will automatically free up memory allocated to the null object, hence deleting it from the WeakSet.
This is wonderful because the WeakSets objects set to null are garbage-collected while the program is still running, hence, reducing memory consumption and preventing memory leakage, especially when dealing with huge amounts of data that are generated asynchronously.
Within this feature lies a chance for you to write light-weight solutions to programming problems without having to bother with the details of memory management.
JavaScript Maps are objects designed to efficiently store and retrieve items based on a unique key for each item. A Map stores key-value pairs where both keys and values could be either primitive values or objects, or both.
The MDN docs describe the Map object as:
The Map object holds key-value pairs and remembers the original insertion order of the keys. Any value (both objects and primitive values) may be used as either a key or a value. A Map object iterates its elements in insertion order — a for…of loop returns an array of [key, value] for each iteration.
Akin to Sets, Maps are easy to create. Let’s create a Map using the Map constructor:
const users = new Map();
console.log(users); // Map {}
Key-value pairs are added to a Map using the set()
method. This method takes in two arguments, the first being the key and the second, the value, which is referenced by the key:
users.set('John Doe', {
email: 'johndoe@example.com',
});
users.set('Jane Doe', {
email: 'janedoe@example.com',
});
console.log(users);
/__ console.log result
Map {
'John Doe' => { email: 'johndoe@example.com'},
'Jane Doe' => { email: 'janedoe@example.com'} }
__/
Unlike Sets which discard duplicate keys, Maps will update the value attached to that key:
users.set('John Doe', {
email: 'johndoe477@example.com',
});
console.log(users);
/__ console.log result
Map {
'John Doe' => {email: 'johndoe477@example.com'},
'Jane Doe' => { email: 'janedoe@example.com'} }
__/
When you run the example above, John Doe's
email will neatly be replaced. Smooth.
As with Sets, key-value pairs can be deleted using the delete()
method. The key to be deleted is passed to the delete()
method as shown below:
users.delete('Jane Doe');
Maps also have a clear()
method, this removes all key-value pairs from the Map object:
users.clear();
console.log(users); // Map {}
Maps also have a has()
method which checks if a key exists in a Map. This method will return true
if the key is in the Map and false
if it is not:
let users = new Map();
users.set('John Doe', {
email: 'johndoe@example.com',
});
users.set('Jane Doe', {
email: 'janedoe@example.com',
});
console.log(users.has('John Doe')); // true
The value of a key in a Map object can be gotten using the get
method on the Map prototype:
console.log(users.get('Jane Doe'); // { email: 'janedoe@example.com' }
It is possible to get all the keys and values of a Map object using the keys()
and values()
methods respectively. These methods both return a new MapIterator
object which has a next()
method that can be used to loop through the items of the Map:
let userKeys = users.keys();
console.log(userKeys.next()); // { value: 'John Doe', done: false }
let userValues = users.values();
console.log(userValues.next()); // _{ value: { email: 'johndoe@example.com' }, done: false }_
As with Sets, loop constructs such as for...of
and forEach()
can be used to loop through Map items:
for (let user of users) {
console.log('[for...of]: ', user);
}
/_ Log result
_[_for...of]: _[_ 'John Doe', { email: 'johndoe@example.com' } ]
_[_for...of]: _[___ 'Jane Doe', { email: 'janedoe@example.com' } ]
_/
users.forEach((value, key) => console.log('[__forEach()]: ', key, value));
/*_ Log result
[__forEach()]: John Doe { email: 'johndoe@example.com' }
_[_forEach()]: Jane Doe { email: 'janedoe@example.com' }
*_/
As with WeakSets, WeakMaps differ from regular Map objects. WeakMaps only accept objects as keys, are not iterable and do not have a clear()
method.
A WeakMap constructor is used to create a WeakMap.
Let’s look at an example:
let users = new WeakMap();
const user1 = {
name: 'John Doe',
};
const user2 = {
name: 'Jane Doe',
};
users.set(user1, {
email: 'johndoe@example.com',
});
users.set(user2, {
email: 'janedoe@example.com',
});
As with WeakSets, setting the key of a WeakMap object to null will implicitly garbage collect that item:
user1 = null;
This has the same advantages as with WeakSets in providing easier memory management.
In this article, we’ve taken a look at Sets and Maps and how they handle unique items and key-value pairs respectively. These useful data structures provide easier and more efficient ways to structure and access data under certain use cases.
Special modifications such as WeakSets and WeakMaps provide more options for the developer and are handy for memory management.
#javascript #es6