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.