New Built-in Classes: Set, Map, WeakSet, and WeakMap

We often use collections of objects when programming. In other languages, you may have used arrays, lists, sets, associative maps, or dictionaries. Sadly, JavaScript offered only arrays in the past.

When programmers needed a set or a map, they resorted to workarounds. These workarounds made the code hard to maintain, and when the code resulted in unintended behavior, we had more errors to deal with.

JavaScript now offers built-in classes for sets and maps as first-class citizens in the language. We will take a look at sets first and then maps. We will wrap up by looking at some special variations of these two classes for better memory use.

Using Set

The Array class that has been available in JavaScript from the beginning is sufficient to deal with an ordered collection of data. However, what if you wanted to create an unordered collection? What if you needed values held in the collection to be unique? Say you want to keep a collection of credit cards that belong to a user. You may want to query whether a particular credit card exists in your collection. Array is a poor choice for such operations; we really need a Set. JavaScript has finally agreed with that sentiment.

A set is a unique collection of primitives and objects—duplicates are not allowed. We can create an empty set and add objects to it, or initialize a set with the contents of an iterable, like an array.

Here’s a set of names with five values, but one of the values presented during construction is not included in the set due to duplication.

 const​ names = ​new​ Set([​'Jack'​, ​'Jill'​, ​'Jake'​, ​'Jack'​, ​'Sara'​]);

Use the property size to get the current number of elements in the set.

 console.log(names.size);

This will return a value of 4 for the given set. Whether we created an empty set or a set with values as we did in the previous example, we can add elements to an existing set. For example, let’s add a new value to an existing set.

 names.add(​'Mike'​);

One nice feature of the add() method is that it returns the current Set—that makes it convenient to chain operations, like more calls to add() or other methods of Set, like so:

 names.add(​'Kate'​)
  .add(​'Kara'​);

We can use the has() method to check whether a set has a particular element. Likewise, we can empty out an existing set using the clear() method or remove an existing element using the delete() method.

To get all the values from a Set, use either keys() or values(). Thanks to the presence of the entries() method, we can also iterate over a Set using the enhanced for loop, like so:

 for​(​const​ name ​of​ names) {
  console.log(name);
 }

If you prefer the functional style internal iterator instead, Set has you covered with forEach:

 names.forEach(name => console.log(name));

Talking about functional style, you may be wondering about methods like filter() and map(). Sadly, Set does not offer these methods, but there’s a workaround. We can create an array from the set, as we concatenated arrays in The Spread Operator, and then use the functional style methods on the array. For example, let’s use filter(), map(), and forEach() to pick only names that start with ’J’, transform to uppercase, and print.

 [...names].filter(name => name.startsWith(​'J'​))
  .map(name => name.toUpperCase())
  .forEach(name => console.log(name));

Use Array to create an ordered collection of values and Set for a collection of distinct values. Besides a collection of values, JavaScript now offers a way to create a dictionary of keys and values, as we’ll see next.

Using Map

Associative maps or dictionaries are significant data structures in programming. You may have used Map in Java or Dictionary in C#, for example. Suppose you want to keep track of teams and scores during the sports season. A map will make it easy to create and update the score values and also to look up the scores based on a team’s name. It’s hard to believe that we can seriously program without maps—we can’t.

Since an object in JavaScript has keys and values, and since there was no special Map type, in the past programmers often used simple objects to represent maps. Unfortunately, this resulted in a few problems. For one, there was no foolproof way to iterate over the keys—the keys() method converted fields to strings and that resulted in the accidental collision of keys. Also, there was no easy way to add new keys and values; in short, using a generic object to represent a map was not intuitive or elegant. The new Map type in JavaScript fixes that issue.

A Map is an associative collection of keys and values where the keys are distinct. Keys and values may be any primitive or object. We can create an empty map and then add values to it, or we can create it with some initial values.

Let’s create a Map of names as the key and some scores as values.

 const​ scores =
 new​ Map([[​'Sara'​, 12], [​'Bob'​, 11], [​'Jill'​, 15], [​'Bruce'​, 14]]);
 
 scores.​set​(​'Jake'​, 14);
 
 console.log(scores.size);

The scores Map has been initialized with a collection of names and scores—the initial data for the map may be any iterable with a pair of keys and values. After creation we added another key and value to the Map using the set() method. To find out the number of keys currently in the Map, we use the size property.

To iterate over the collection of keys and values, we use the entries() method. Since it returns an iterable, we can use the enhanced for loop along with destructuring. For example, let’s extract the name and score for each key-value pair and print:

 for​(​const​ [name, score] ​of​ scores.entries()) {
  console.log(​`​${name}​ : ​${score}​`​);
 }

Instead of using the external iterator, we can also use the internal iterator, forEach(), to iterate over the keys and values, but the sequence of parameters provided to the function passed to forEach() is rather odd. Let’s take a look at an example to iterate and then discuss the parameters.

 scores.forEach((score, name) => console.log(​`​${name}​ : ​${score}​`​));

The first parameter received by the function is the value for a key that appears as the second parameter. It is more natural to think of receiving key and value instead of value and key, but the reason for this sequence is that the same forEach() method may be used to iterate over only the values:

 scores.forEach(score => console.log(score));

If you receive only one parameter, that ends up being the value; if you receive two parameters, then they stand for value and key for each key-value pair in the Map.

If you like to iterate over only the keys, then use the keys() method, and to get an iterable of only the values use the values() method. Finally, to query whether a key exists, use the has() method.

WeakSet and WeakMap

Suppose you added an object as a value to a Set or as a key to a Map. If that object is no longer needed in your application, it can’t be garbage collected. The Set or the Map that holds on to the object will prevent it from being cleaned up. This is not very gentle on memory usage and may be an issue in some applications that use a large amount of data. WeakSet, a counterpart of Set, and WeakMap, a counterpart of Map, can be used to solve this issue, since both have a minimal impact on memory usage.

The word weak refers to coupling, as in weak coupling. A Set, for example, tightly holds on to the data that is added. However, a WeakSet will hold on only weakly and will not prevent the object from being released. Let’s discuss why we may need a weak collection before looking at the built-in weak collections in JavaScript.

Suppose you have an application where information about various vehicles based on vehicle identification number (VIN) is obtained from a database. Each time you need that information, you may not want to perform a fetch. It may be far more efficient to cache that information, for example, in a Map. However, if the number of vehicles the application deals with becomes large, and as time goes on, some of the vehicles are no longer being processed, then you’d have to explicitly remove those vehicle details from the cache. Otherwise, the application may suffer from excessive memory usage.

This is where a WeakMap can help. Now suppose, instead of using Map for the cache we used a WeakMap. When a VIN is discarded, the data associated with that VIN as key within the WeakMap becomes stale and becomes a candidate for garbage collection. When the memory demand increases, the runtime may perform an automatic cleanup of these stale objects without any explicit programming effort on our part. A big win for less code and efficient memory usage at the same time.

Another scenario where a weak collection is useful is in GUI programming. A UI control may be added to a collection so that events can be sent to it. However, if the UI control is no longer needed, we would not want it to be held in the collection. If we used a Set, for example, we will have to do an explicit delete. A WeakSet can release the object automatically when it is no longer needed in the program.

The values stored in a WeakSet and the keys in a WeakMap may be garbage collected if they’re not otherwise needed in the application. Thus, these objects may go away at any time without notice. To avoid any surprises, WeakSet and WeakMap have some restrictions on how we can access the elements:

  • While the values stored in a Set and the keys placed into a Map may be primitive or objects, the values in a WeakSet and keys in a WeakMap are required to be objects, not primitives.

  • The weak collections are not enumerable. The reason for this is while in the middle of enumeration, if it were possible, an object held in the collection may be garbage collected and that would throw a wrench into the iteration.

The WeakSet provides only the methods add(), delete(), and has() to work on the elements in the collection. The WeakMap provides the get(), delete(), set(), and has() methods. Just as we can’t enumerate on a weak collection, we can’t query for its size either—there’s no size property.

Let’s compare the behavior of Map and WeakMap. Here’s an example that puts a lot of elements into an instance of Map:

 const​ MAX = 100000000;
 const​ map = ​new​ Map();
 
 for​(​let​ i = 0; i <= MAX; i++) {
 const​ key = {index: i};
  map.​set​(key, i);
 }
 
 console.log(​"DONE"​);

Through each iteration a new object is created on the heap and then inserted as a key into the map with a primitive value as the value for the key. Each key that is created on the heap during an iteration may be garbage collected at the end of that iteration; however, the Map will keep them alive. The result of this code is an error due to excessive memory usage:

 ...
 FATAL ERROR: invalid table size Allocation failed -
 JavaScript heap out of memory

If on your system you do not get an out-of-memory error, try increasing the value of MAX in the previous code by an order of magnitude until you get the error—and congratulations on having a machine with a lot of memory.

Let’s change new Map() in the previous code to new WeakMap():

 //...
 const​ map = ​new​ WeakMap();
 //...

Now, let’s run the code. It may take a while to complete, but it eventually will without any errors:

 DONE

The example shows that while Map prevents objects, which are otherwise not needed, from being garbage collected, the WeakMap doesn’t cling on to the objects. sIf the objects gotta go, they gotta go—weak collections don’t prevent that.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.98.166