- 6 minutes read

In JavaScript, everything's a map. So why did EcmaScript 6 introduce Map?

The traditional approach

It took me a while to wrap my head around it. JavaScript is a slot machine. Objects are racks with slots. You can put functions and values into these slots. Just like in real life, you can empty a slot again, or you can put other stuff into such a slot later. That's why the key-value-pair is called a variable, or in object-oriented lingo, an attribute.

Unlike many other languages, JavaScript uses the same concept for slots containing methods. A method is merely a slot containing a function. Again, that's just a key-value-pair. You can add methods to an existing object, you can replace methods with other methods, and you can even delete methods from an object.

Compare this to a hash table. Can you spot the difference? Yeah, there's none. Hash tables (also known as maps) are simple lists of key-value pairs.

Easy does it!

So if you need a hash map in JavaScript, simply take an object. How cool is that?

const trafficLights = { 'green': 'go', 'yellow': 'be careful', 'red': 'stop' }; console.log(trafficLights.red); console.log(trafficLights['green']);

Iterating

Using objects as maps is fun. The syntax is intuitive. The concept is deeply ingrained in the language, so every modern JavaScript engine implements it very efficiently. Great. Until you need to iterate the elements of the map. At this point, things become a bit clumsy. The good news is that JavaScript is so incredibly flexible it supports this use-case, too. But brace yourself for a few surprises:

for (const color in trafficLights) { console.log(color + " means " + trafficLights[color]); }

Well... I had some trouble with this kind of for loop a couple of days before writing the article. However, now that I'm writing this article, running the code in Node.js or Chrome yields the expected result. It iterates over the three colors most traffics lights show.

Nonetheless, a small variation of the source code shows it's fishy:

console.log('toString' in trafficLights); // yields true

Under certain circumstances, the for...in loop not only list the properties of the map, but also the properties it's inheriting from the prototype. So it's better to guard your for loop with a check like so:

for (const color in trafficLights) { if (trafficLights.hasOwnProperty(color)) { console.log(color + " means " + trafficLights[color]); } }

Dictionaries: getting rid of the prototype

You can get rid of the prototype by declaring the prototype of the map explicitly. We're only interested in the data structure, so we can get rid of the prototype altogether by setting it to null:

const trafficLights = Object.create(null); trafficLights.green = "go"; trafficLights.yellow = 'be careful', trafficLights.red = 'stop';

Here's the catch: this version is much slower than using object literals. That and the verbosity of this version is the reason why you'll see it hardly ever in the wild.

Convenience methods

You can use the methods keys(), values(), and entries() to iterate more conveniently. Just remember the syntax. It's not trafficLights.values(), but Object.values(trafficLights):

for (var entry of Object.entries(trafficLights)) { console.log(entry[0] + ' means ' + entry[1]); }

Also, keep in mind that the method entries() doesn't yield an array of objects, but an array of arrays.

Performance

Back to the performance topic. Object literals are also slow if you're modifying the hash map. In particular, adding and removing key-value-pairs are slow operations.

In the case of the delete operation, that's obvious. Remember, object literals are basically objects. While it's not entirely unheard of, deleting methods or properties from a parent object is a very exotic use case. The idea of object-oriented inheritance is to find the greatest common denominator of two classes, moving the common functionality to the superclass. Few people define functionality in such a superclass, only to forget it in the child class.

Inserting items is a similar story. Internally, the V8 engine of Node.js and Chrome compiles objects to classes, if they are frequently accessed. The difference is that V8 assumes that classes don't change over time, so it can optimize them aggressively. That's obviously a wrong assumption in our case, so the JavaScript engine has to de-optimize the class frequently.

Introducing Map

EcmaScript 6 addressed these problems by introducing a new, dedicated data type. Map is a generic container type addressing several issues:

  • It iterates without surprises.
  • It's optimized for adding items to and removing items from the map.
  • The keys can by any type, not just strings.
  • You can't pollute it with custom methods. Or rather, probably you can add your custom method to a Map, but nobody does. The semantics of Map are inheritently clear.

So the general recommendation is to use object literals for the simple cases, and Map if you need a sophisticated data structure. The syntax is slightly different. It almost follows the Java conventions, but uses different method names:

const trafficLights = new Map(); trafficLights.set('green', 'go'); trafficLights.set('yellow', 'be careful'); trafficLights.set('red', 'stop'); console.log(trafficLights.has('green')); console.log(trafficLights.get('green')); for ([color, meaning] of trafficLights.entries()) { console.log(color + ' means ' + meaning); }

WeakMap

In the era of single-page applications, memory considerations have arrived in the JavaScript world. Hash maps are often used for things like caching. You want to be able to access an object quickly, but if the garbage collector of the virtual machine decides that the object can safely be removed, that's fine by you. The problem is that the ordinary Map contains a pointer to the object. That, in turn, tells the garbage collector someone's using the object.

You can get rid of this potential memory leak by using a WeakMap. However, WeakMap has several limitations. It doesn't support iteration, and there's no size attribute. Plus, you never know when garbage collection occurs. Read the full details here.

Wrapping it up

Most seasoned JavaScript programmers use an object literal if they need a hashmap. That's a nice and concise syntax which has been adopted by many other languages, such as Groovy.

If you've got a Java programmer in your JavaScript team, they'll almost certainly discover the new ES6 type Map before learning about the object literals. In a way, that's good. Map has many advantages. If the hashmap is modified frequently, Map offers better performance. If you're using TypeScript, it's even a generic type, protecting you with type safety.

Just keep in mind that Map is a completely different implementation than the object literals. In particular, the API is different. You can mix both approaches. You can't convert an object to a Map directly (or vice versa). You have to use Object.entries() for that:

const map = new Map(Object.entries({foo: 'bar'}));


Dig deeper

ES6 Map vs WeakMap vs plain Objects

ES6 — Map vs Object — What and when?

Stackoverflow: Map vs. Object

ES5 Objects vs. ES6 Maps – The differences and similarities

Any point in using ES6 Map when keys are all strings?

Map, Set, WeakMap and WeakSet

True hash maps in JavaScript

How to convert a plain object into an ES6 map


Comments