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:
- Using named captures
- Object spread vs.
Object.assign
- Converting an object to
Map
and vice-versa (this post) - The
for
-of
loop: should there remain only one… - Simulating an abstract class with
new.target
- 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 afetch
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 anObject.create(null)
instead of a{}
, or always access through appropriate APIs (such asObject.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
for
…in
,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
for
…of
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!