Carsten Ruhr

Map and WeakMap in JavaScript

In JavaScript most of the data structures are represented either as primitives, arrays or objects (I know, strictly speaking arrays are objects - but you know what I mean). And for most cases, that's all you need. But sometimes you find yourself in a spot wanting some extra functionality or comfort.

Let's say for example you want to iterate over an object. Quite easy, right?

for([key, value] of Object.entries(myObject)) {
	...
}

But that tells us an interesting fact about objects. They are not iterable on itself. You always have to use Object.keys(), Object.values() or Object.entries()to get an array.

Maps on the other hand, are iterable without any further steps.

for([key, value] of myMap) {
	...
}

It seems like a minor improvement, but it's not only syntactically more pleasant but also more performant.

Another difference is the type of key you are allowed to use. In objects you can use strings or integers (which internally are handled as strings anyway). In maps you can use everything as a key you can find. Like... another map, for example. Crazy, right?

const map1 = new Map()
const map2 = new Map()

map1.set('Hello', 'map')
map2.set(map1, 'My key is a map.')

// will print "My key is a map"
map2.get(map1)

If you wonder what the use case for such a weird construct could be - I wrote a few words about that at the end of the article.

Objects also don't preserve the original order of elements while maps do it.

const myObject = {'_': 0, 'a': 1, '1': '2'}

// will print { 1: "2", _: 0, a: 1 }
console.log(myObject)

const myMap = new Map()
myMap.set('_', 0)
myMap.set('a', 1)
myMap.set('1', 2)

// will print { _ → 0, a → 1, 1 → 2 }
console.log(myMap)

Having the same order in key → value pairs probably is rarely important. But it's nice to have the knowledge that there is an option besides nested objects with explicit order properties. A limitation of maps is that you can not easily insert entries at a defined position. One solution to this problem is to convert the map to an array via Array.from(myMap), use splice()to insert your new element at the position you want it to be and then clear your old map and re-insert all the entries from the array.

const myMap = new Map()
myMap.set('_', 0)
myMap.set('a', 1)
myMap.set('1', 2)

// the following 4 lines could be an own function for re-usability
const tmpArr = Array.from(myMap)
tmpArr.splice(1, 0, ['Hello', 'World'])
myMap.clear()
tmpArr.forEach(([key, value]) => myMap.set(key, value))

// will print { _ → 0, Hello → "World", a → 1, 1 → 2 }
console.log(myMap)

This is not very efficient but should be fine for most cases.

So what are WeakMaps then?

WeakMaps are maps in which the keys can ONLY be objects. Primitives are not allowed. And those objects are weakly referenced by the map. That means the garbage collector doesn't care about the link between object and map. If it's not used anywhere else than in the map it will be removed from the memory.

Why would you want that?

The problem with objects as keys in regular maps is that they will never get garbage collected as long as both, the object itself AND the entry in the map, have not been deleted. Let's assume the following scenario:

const myObject = { 'Hello': 'Object' }
const myMap = new Map()
myMap.set(myObject, 'fooBar')

// here we attempt to "delete" the object
myObject = null

// some time passes, the garbage collector runs at some point
[...]

// will contain the element { Hello: "Object" } → "fooBar" even though we deleted myObject earlier.
console.log(myMap)

// will print "undefined" since myObject is not referencing the original object anymore
console.log(myMap.get(myObject))

Congratulations, you just created a memory leak. This entry in the map is not going anywhere unless you close (or refresh) the browser tab.

And this is where you can use WeakMap instead of Map. Once the object is set to null and the garbage collector does his thing the entry in the WeakMap will disappear and memory will be freed.

Are there any valid use cases for objects as keys?

Yes. Thanks for asking!

Maps with objects as keys can be used to add information to an object without actually altering the object itself. We could call them metadata or whatever. Here is a very simple example:

const metaData = new WeakMap()

const johnDoe = {
	getMetaData: function() {
		return metaData.get(this)
	}
}

johnDoe.name = "John Doe"
johnDoe.dateOfBirth = "21/3/3"

metaData.set(johnDoe, { "favouriteFood": "Pizza" })

// will print { favouriteFood: "Pizza" }
console.log(johnDoe.getMetaData())

So now we can delete the object johnDoe and don't have to worry about the metadata. The example above makes not much sense, of course. But there are other cases where you want to enrich an object with additional data without mutating the object itself (let's say metadata for DOM nodes for example).

Learn more about Map here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
And more about WeakMap here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap