Converting an object to Map and vice-versa
Published on 19 May 2020 • 3 min

Cette page est également disponible en français.

We’re already at the sixteenth post in our daily series “19 nuggets of vanilla JS.” Today is about one of the new collection types that appeared in ES2015: Map. When should you use it instead of a plain object, and how to easily switch between these two representations?

The series of 19

Check out surrounding posts from the series:

  1. Using named captures
  2. Object spread vs. Object.assign
  3. Converting an object to Map and vice-versa (this post)
  4. The for-of loop: should there remain only one…
  5. Simulating an abstract class with new.target
  6. Surprise!

Object, an easy dictionary

JavaScript has always used good ol’ objects to pair keys and values (and starting with JS 1.1, object literals made it much more concise):

// Initial definition
const classyGuy = {
first: 'Georges',
last: 'Abitbol',
year: 1992,
}

// Static (hardcoded) reading
classyGuy.first
// Dynamic reading
classyGuy[formal ? 'last' : 'first']
// Presence testing (with or without inherited properties)
'year' in classyGuy
classyGuy.hasOwnProperty('year')
// Adding
classyGuy.title = 'The world’s classiest guy'
// Updating
classyGuy.year = 1993
// Removing
delete classyGuy.year
// Enumerating
for (var prop in classyGuy) {
console.log(prop, '=', classyGuy[prop])
}

As new versions came out (ES3, ES5, ES2015…), many static APIs appeared to introspect objects:

Object.keys(classyGuy)
Object.values(classyGuy)
Object.entries(classyGuy)
Object.getOwnPropertyNames(classyGuy)
// etc.

Awesome! But then, why did we need Map?

Map: what benefits does it bring?

Using plain objects for our dictionaries is indeed convenient and concise, but does suffer from pretty stark limitations:

  • Keys have to be of type String (or, since ES2015, Symbol): we can’t use a custom object of ours, or a host object (e.g. a DOM node or a fetch request) as key.
  • There is confusion between inherited and own properties (not to mention enumerability). In practice, as we often use plain objects, we only inherit a few things from Object (toString, valueOf, hasOwnProperty and a few more), with names that bear a rather low collision risk. But still, as an extra precaution, we should either start from an Object.create(null) instead of a {}, or always access through appropriate APIs (such as Object.getOwnPropertyNames(…), hasOwnProperty(…), etc.).
  • No easy way to clear the dictionary. No Object.clear() or some such: if you want to retain container identity but clear it out, you’re in for some tedious code.
  • Iteration order is not guaranteed. Even if, in practice, the order used by forin, Object.keys() and friends is usually the chronological order of addition, the spec doesn’t mandate it and variations do exist. ES2020 added some clarity, but still.
  • Not iterable by default. A plain object is not iterable (in the ES2015 sense) by default, meaning you can’t immediately use it with spreads, positional destructuring, the forof loop or any other means of consuming an iterable (e.g. parts of the standard library).
  • Performance suffers in mutation-heavy scenarios. To properly optimize the indexing of the object (the access to its properties), JS engines need the object’s “shape” to remain stable: most of the time, changing that shape by adding or removing properties invalidates optimized lookup caches. So if you find yourself adding or removing properties a lot in your dictionary, performance is going to suffer.

This is why the standard library added Map with ES2015. At the cost of having to use a more explicit, slightly more verbose API (and having to convert for JSON (de)serialization), you get a number of benefits:

  • Keys can be anything (even undefined is an acceptable key)
  • Optimal performance
  • Iterable by default
  • Richer API (including clearing)
// Initial definition
const classyGuy = new Map([
['first', 'Georges'],
['last', 'Abitbol'],
['year', 1992],
])

// Reading
classyGuy.get('first')
// Presence testing
classyGuy.has('year')
// Adding / updating -- keys can have any type!
classyGuy.set('title', 'The world’s classiest guy')
classyGuy.set(classyGuy, 'OMG SO META')
classyGuy.set(null, 'Null this over')
// Removing
classyGuy.remove('year')
// Enumerating (tons of ways, here’s my go-to take)
for (const [key, value] of classyGuy) {
console.log(key, '=', value)
}
// Clearing
classyGuy.clear()

How can we switch between the two?

You might need, from time to time, to turn a Map into a plain object. Perhaps you want to serialize it as JSON before sending it over the wire or persisting it on disk (and reciprocally, you’d like to turn it back into a Map after fetching it or reading it from disk).

If you Map has only String or Symbol keys, this is a one-liner:

// Map -> Object (ES2019+)
Object.fromEntries(map.entries())

// Object -> Map (ES2017+)
new Map(Object.entries(obj))

ES2018 finally brings Object.fromEntries(…), the inverse operation of Object.entries(…) that came with ES2017 (a long-awaited extension there too), which was already a neat addition to ES5’s Object.keys(…) (from 2009).

This is easy to polyfill (through the usual means: core-js, polyfill.io, etc.) so even IE9+ could use it (and you can polyfill Map too).

If you absolutely must limit yourself to ES2015 without polyfills (but why? Are you that masochistic?), you can emulate that with a huge beast of an expression involving map.entries(), Array.from, reduce, property descriptors and Object.create. As academic literature is fond of saying, this is “left as an exercise for the reader”.

Where can I get it?!

  • Object.entries(…) has been supported since Chrome 54, Edge 14, Firefox 47, Opera 41, Safari 10.1 and Node 7.
  • Map has been native since Chrome 38, Edge 12, Firefox 36 (even Fx20 for what we need here!), Opera 25, Safari 8 and Node 4.
  • Object.fromEntries(…) showed up in Chrome 73, Edge 79, Firefox 63, Opera 60, Safari 12.1 and Node 12.

Again, this is all easy to polyfill anyway.

Want to dive deeper?

Our trainings are amazeballs, be they in-room or remote online, multi-client or in-house just for your company!