Maps in Javascript ES6

Introduction

ES6 introduced a lot of new things. I’ve already write about Sets, so today we’ll explore Maps. What are those? A Map is an unordered list of key-values pairs where the key AND the value can be of any type.

Problems solved

Developers have tried to implement maps before ES6, but some issues arise because of the way object properties are handle in Javascript. In an object, every property must be a string. So, if you give an object a key with a different type, it will be coerced into a string.

let map = {}

map[5] = 4
map[{}] = 'An object'

// { '5': 4, '[object Object]': 'An object' }

As you can see, our 5 became ‘5’, our empty object became ‘[object Object]’. That’s some serious limitations there!

In ES6, Maps use the Object.is() method to compare keys, just like Sets do with their values. Maps also do not make every key a string, every type is allowed.


Object.is(5, '5') // false Object.is({}, {}) // false

Constructor

So, how to create a new Map? By using new Map(). You can also initialize a map with an array of arrays:

const map = new Map()
// Map {}

const map = new Map([[5, 42], ["name", "Paul"], ["age", 45]])
// Map { 5 => 42, 'name' => 'Paul', 'age' => 45 }

In the array of arrays, each array represents a key-value pair. The first item in each array will become the key, the second will be the value. The structure may look odd, but it is the best way to make sure we can allow any type of data for keys.

Maps methods

To interact with a map, you have a few methods at your disposal.

  • The set(key, value) method adds a pair to the map.
  • The get(key) method retrieves a value from the map. The get method will return undefined if nothing has been found.
  • The has(key) method checks if the key exists in the map. Returns true or false.
  • The delete(key) method removes the key and its value from the map.
  • The clear() method removes all keys and values from the map.
  • Finally, maps have a size property that returns the number of key/value pairs in the map.
const map = new Map()

map.set(5, "Hello")
map.set("5", "World")
map.set("John", "The revelator")
map.size // 3
// Map { 5 => 'Hello', '5' => 'World', 'John' => 'The revelator' }

map.get(5) // Hello
map.has('5') // true
map.get('Random') // undefined
map.has('John') // true

map.delete('5')
map.size // 2
// Map { 5 => 'Hello', 'John' => 'The revelator' }

map.clear()
map.size // 0
// Map {}

Objects keys in map

As I mentioned earlier, objects can be used as keys in a map.

const map = new Map()
let obj1 = {}
let obj2 = {}

map.set(obj1, 12)
map.set(obj2, "OBJECT")
map.size // 2
// Map { {} => 12, {} => 'OBJECT' }

As you can see, even though we are are using two empty objects as keys, we are using the references of those objects in the map. Therefore, Object.is(), who is used for comparing the keys, returns false. Again, notice that the object are not coerced into strings.

Iteration

You can iterate through a Map by using forEach(). The callback passed receives three arguments: the value, the key and the map we are using.

const map = new Map([[5, 42], ["name", "Paul"], ["age", 45]])

map.forEach((value, key, thisMap) => {
    console.log(`${key} => ${value}`)
    console.log(thisMap === map)
})

//5 => 42
//true

//name => Paul
//true

//age => 45
//true

Weak Maps

Weak maps obey the same principle of weak sets. In a Weak Map, every key must be an object. Weak maps are used to store weak object references. What does that mean?

const map = new Map()
let obj1 = {}
map.set(obj1, 12)
//Map { {} => 12 }
obj1 = null // I remove the obj1 reference
// Map { {} => 12 } // But the reference still exists in the map anyway

In this case, our object’s reference still exists in the map. Removing the reference everywhere else does not remove it from the map. It is not garbage collected to free memory. In certain cases, you would want to optimize memory usage and avoid memory leaks. This is what a WeakMap does for you. If the reference of an object disappear everywhere else in your program, it will be removed from the WeakSet also.

const map = new WeakMap()

let obj = {} // creates a reference to obj
map.set(obj, 12) // stores the reference inside the WeakMap as a key
map.has(obj) // true
map.get(obj) // 12

obj = null /* removes the reference. Will also remove it from the WeakMap because there are no other references to this object */

map.has(obj) // false
map.get(obj) // undefined
console.log(map) // WeakMap {}

// obj is gone from the WeakMap

Note: This only works when objects are stored as keys, not values. If an object is stored as a value and all other references disappear, it will not disappear from the WeakMap. Weak map keys are weak references, not weak map values.

You can also initialize a WeakMap with an array of arrays, just like a Map. The difference is that because every key must be an object, the first item in each array must be an object. An error will be thrown if you try to put a nonobject key inside a WeakMap.

Note: WeakMap does not have a size property

Weak Maps use cases

One possible use case for WeakMap could be when you are tracking DOM elements. By using a WeakMap, you could store DOM elements as keys. As soon as the element is removed, the object will be garbage collected to free memory.

const map = new WeakMap()
const element = document.querySelector(".button")

map.set(element, "Buttons")

map.get(element) // "Buttons"

element.parentNode.removeChild(element) // remove the element
element = null // removes reference

// WeakMap now empty!

One other practical use of WeakMap is to store private object data. All object properties are public in ES6. So how would you go about it? In ES5, you could do something like this:


var Car = (function(){ var privateCarsData = {} var privateId = 0 function Car(name, color){ Object.defineProperty(this, "_id", {value: privateId++}) privateCarsData[this._id] = { name: name, color: color } } Car.prototype.getCarName = function(){ return privateCarsData[this._id].name } Car.prototype.getCarColor = function(){ return privateCarsData[this._id].color } return Car }())

This is as close as you’ll get to have truly private data in ES5. Here, the Car definition is wrapped inside an Immediately Invoked Function Expression (IIFE). We have two private variables, privateCarsData and privateId. privateCarsData stores private information for each Car instance and privateId generates a unique id for each instance.

When we call Car(name, color), the _id property is added inside privateCarsData and this receives an object with name and color properties. *getCarName* and *getCarColor* retrieve data by using this._id as the key.

The data is safe because privateCarsData is not accessible outside the IIFE, this._id is exposed however. The problem is that there are no ways to know when a Car instance is destroyed. Therefore, we can’t update privateCarsData appropriately when an instance disappear and it will always contain extra data.

const Car = (function(){

    const privateCarsData = new WeakMap()

    function Car(name, color){
        // this => Car instance
        privateCarsData.set(this, {name, color})
    }

    Car.prototype.getCarName = function(){
        return privateCarsData.get(this).name
    }

    Car.prototype.getCarColor = function(){
        return privateCarsData.get(this).color
    }

    return Car
}())

This version uses a WeakMap for the privateCarsData instead of an object. We will use the Car instance as the key, so we don’t need to generate a unique id for each instance. The key will be this and the value is an object containing the name and the color. getCarName and getCarColor retrieve the values by passing this to the get method. And now, whenever a Car instance is destroyed, the key referencing that instance inside the privateCarsData will be garbage collected to free memory.

Conclusion

Anytime you will want to use only object keys, weak maps will be your best choice. Memory will be optimised and memory leaks avoided. However, weak maps give you very little visibility into what they have. You can’t use forEach(), no size property and no clear() method. If you need to inspect the contents, use regular maps. Obviously, if you want to use nonobject keys, you will have to use regular maps too.

Your email address will not be published. Required fields are marked *

*