Map and Set: JavaScript's Iterable Collections

Arrays are Bad

Arrays are bad.

Really? That's the entire section?

I didn't think it needed explanation, but if we must:

As programmers, when we are asked to iterate over a set of data, we immediately assume that the input we will be iterating over is an array. Compared to that other basic structure, the object-hash-dictionary-hashmap-whateverYourLanguageCallsIt, arrays are somewhat nice to iterate over because the computer just has to do some addition to figure out where your data is stored. The computer (and other programmers) have given us plenty of tools aimed at iterating over a declared array. They're also JSON friendly, and you, as a programmer, don't need to spend mental energy thinking about what you want to set as the key pointing to each data point. It'll be a set of consecutive integers starting at 0.

There are quite a few operations that arrays are poorly suited for, however:

  • Arrays have a really difficult time knowing when data is repeated. There are certainly plenty of times where we need to push data into an array and check either during or after the process that we only have unique values. That will require at least one additional loop through the array, and if we want to get our unique items in anything less than O(n2), we are going to use a non-array structure in that process anyway.
  • Arrays don't remember the purpose of their data. The advantage of a programmer not having to think up a key for each data point is that... the data points end up not having useful keys.
  • Removing items from an array is not programmatically easy. Unless you are removing the final (pop) or first (shift) item from an array, you are going to need to write a function that can target the data you want to remove. Then, the computer needs to shift all the other data points after your removed item around (and this goes for when you shift the first item off as well). That's an entire additional iteration through your array, which could be a noticeable amount of time for a large array.
  • Removing items from an array is semantically ugly. Let me know in the comments which of these functions is the prettiest code for a novice JavaScripter to understand. I can't decide: And only one of those actually bothers with manipulating the original array. The others add a new copy.
  • But we know that we can iterate over JavaScript objects as well with for-in loops. Thanks to the magic of hash functions, we can access data in our collection easily. But, the idea of an 'object' does not semantically suggest that it is, in fact, something iterable. And, unlike arrays, objects do not have a set order, which is helpful in many circumstances.

    As of ES6, JavaScript now includes a few built-in objects that combine the key-based lookup and unfussy deletion properties of objects with the ease of iteration and saved chronology of arrays: the Set and Map classes.

    Set

    The Set class has a lot in common with key-value databases like Redis. The methods of adding, removing, checking on, and replacing items in our collection are semantically obvious. Sets know their size, do not duplicate data like arrays, and can still be iterated over.

    The difference between a JavaScript set and a key-value database is that the data is the key and the value is... well... it's just true.

    A developer can ask the set whether it contains a value, and if a developer pushes a value into the set, the set will only allow it if the data is new (tested using '===', so objects will always be considered unique). We could do all of this in an object, but on comparison, the semantic and coding advantages become obvious.

    A JavaScript set is created by declaring a variable a new Set(). After the declaration has been made, the set gains access to the .add, .delete, .has, and .size methods. Other available methods for sets include .clear, .entries (which returns [value, value] for each entry), and .keys and .values (which return the same thing).

    Unlike an array, nothing is repeated. We could call fruits.add(5) all day, but there would only be one occasion of 5 in our set.

    We could create all of these abilities using just an object, but the amount of code we need to write is significant:

    Notice that object iteration does not return the values in order of insertion as set iteration did. Iterating over regular JS objects is independent of their insertion order.

    Map

    While sets are great at remembering their contents, sometimes programmers need a full key-value pair for their data. Sets are limited in that we cannot reach into an object placed a set and return values of that object's keys. Thus, we have Map.

    Maps are a bit more similar to traditional JS objects than sets. The advantages of maps are:

    • Maps do not come with any of the built in default keys that objects do. The object prototype has some default keys that can collide. Object.clear() is a prototype function; so if we were to create a key called 'clear' on our object, we could create problems down the line. Maps separate these issues by placing our key-value pairs one level down from its built-in keys.
    • Maps can use functions and objects as keys. Objects are limited to using strings and symbols as keys.
    • Maps remember their insertion order. Objects just don't care.
    • Maps have access to their size. You would need to iterate over all keys with a counter to determine the size of an object.
    • Speaking of iterating: Maps are iterable without getting the keys. Objects are not.
    • It is easy to remove a key-value pair from a map. Objects, not so much.

    Let's demonstrate these advantages in code:

    The .get and .set methods are used to retrieve and introduce/edit key-value pairs, just like any good caching database. The iteration at the end also produces the data in the same order their keys were placed in the map. .size checks on the number of keys available to our map and .has checks on the existence of the key.

    Other methods a map has access to include .clear, .delete, .entries (returns [key, value]), .keys (returns all keys in an array), and .values (returns all values in an array).

    While maps allow objects and functions to be used as keys, the usefulness of that option is limited, since objects and functions won't be === to a similar object/function.