TL;DR
I introduce MapPlus, a simple class that makes your code easier to read and ensures Maps are easier to reason with.
What is Map
In JavaScript, the Map
is a very useful built-in class that creates an O(1) lookup between a key and a value.
const myMap = new Map() for(const file of files) { const [,extension] = file.name.split(".") if(!myMap.has(extension)) { myMap.set(extension, []) } myMap.get(extension).push(file) }
You can use Maps for all sorts of things you are likely to do regularly:
Creating grouped lists of data, like the above example grouping by file extension
Aggregating data, like counting or summing values across a range of keys
const items = ['apple','apple','orange','banana','apple']; const counts = new Map(); for (const item of items) { counts.set(item, (counts.get(item) || 0) + 1); }
- Creating rapid lookups to be used in subsequent steps
const users = [ {id:1,name:'A',role:'admin'}, {id:2,name:'B',role:'user'}, {id:3,name:'C',role:'user'} ]; const userMap = new Map(); for (const u of users) { userMap.set(u.id, u); }
Why use Map?
Map is preferred to using a simple object ({}
) for a couple of reasons, so long as you don't have to store the result using a stringify
:
- It can take keys which are not strings
- It's slightly faster than an Object even if you are using string keys
There can be a lot of boilerplate and mixed concerns though, if the object you are storing in the map needs construction, which is anything from a simple array to a complex object, this needs to be interspersed with the code that uses it.
const map = new Map() for(const item of items) { if(!map.has(item.type)) { const newType = new Type(item.type, getInfoForType(item.type)) map.set(item.type, newType) } map.get(item.type).doSomething(item) }
This "can" be ok, but it becomes harder to keep DRY if you need to update or initialise the value in multiple places.
For this reason I use a MapPlus class, which is an extension to Map that provides a missing key initialiser function that can be supplied to the constructor or as a second parameter to the get
if the initialiser does need in context information beyond just the key.
The MapPlus Class
class MapPlus extends Map { constructor(missingFunction) { super() this.missingFunction = missingFunction } get(key, createIfMissing = this.missingFunction) { let result = super.get(key) if (!result && createIfMissing) { result = createIfMissing(key) if (result && result.then) { const promise = result.then((value) => { super.set(key, value) return value }) super.set(key, promise) } else { super.set(key, result) } } return result } }
With this you can just do things like:
const map = new MapPlus(()=>[]) for(const item of items) { map.get(item.type).push(item) }
If the key is missing it will just make an empty array, but the loop itself is kept simple and clean, concerned only with making the list.
I often need two levels of this so I'll have maps defined like this:
const map = new MapPlus(()=>new MapPlus(()=>[])) for(const item of items) { map.get(item.type).get(item.subType).push(item) }
The constructor function does get the key
being used so we can also do:
const map = new MapPlus((type)=>new Type(type, getInfoForType(type)) for(const item of items) { map.get(item.type).doSomething(item) }
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.