Maps in ECMAScript 6

The ECMAScript 6 Map type is an ordered list of key-value pairs, where both the key and the value can have any type. Keys equivalence is determined by using the same approach as Set objects, so you can have both a key of 5 and a key of "5" because they are different types. This is quite different from using object properties as keys, as object properties always coerce values into strings.

You can add items to maps by calling the set() method and passing it a key and the value to associate with the key. You can later retrieve a value by passing the key to the get() method. For example:

  1. let map = new Map();
  2. map.set("title", "Understanding ES6");
  3. map.set("year", 2016);
  4. console.log(map.get("title")); // "Understanding ES6"
  5. console.log(map.get("year")); // 2016

In this example, two key-value pairs are stored. The "title" key stores a string while the "year" key stores a number. The get() method is called later to retrieve the values for both keys. If either key didn’t exist in the map, then get() would have returned the special value undefined instead of a value.

You can also use objects as keys, which isn’t possible when using object properties to create a map in the old workaround approach. Here’s an example:

  1. let map = new Map(),
  2. key1 = {},
  3. key2 = {};
  4. map.set(key1, 5);
  5. map.set(key2, 42);
  6. console.log(map.get(key1)); // 5
  7. console.log(map.get(key2)); // 42

This code uses the objects key1 and key2 as keys in the map to store two different values. Because these keys are not coerced into another form, each object is considered unique. This allows you to associate additional data with an object without modifying the object itself.

Map Methods

Maps share several methods with sets. That is intentional, and it allows you to interact with maps and sets in similar ways. These three methods are available on both maps and sets:

  • has(key) - Determines if the given key exists in the map
  • delete(key) - Removes the key and its associated value from the map
  • clear() - Removes all keys and values from the map

Maps also have a size property that indicates how many key-value pairs it contains. This code uses all three methods and size in different ways:

  1. let map = new Map();
  2. map.set("name", "Nicholas");
  3. map.set("age", 25);
  4. console.log(map.size); // 2
  5. console.log(map.has("name")); // true
  6. console.log(map.get("name")); // "Nicholas"
  7. console.log(map.has("age")); // true
  8. console.log(map.get("age")); // 25
  9. map.delete("name");
  10. console.log(map.has("name")); // false
  11. console.log(map.get("name")); // undefined
  12. console.log(map.size); // 1
  13. map.clear();
  14. console.log(map.has("name")); // false
  15. console.log(map.get("name")); // undefined
  16. console.log(map.has("age")); // false
  17. console.log(map.get("age")); // undefined
  18. console.log(map.size); // 0

As with sets, the size property always contains the number of key-value pairs in the map. The Map instance in this example starts with the "name" and "age" keys, so has() returns true when passed either key. After the "name" key is removed by the delete() method, the has() method returns false when passed "name" and the size property indicates one less item. The clear() method then removes the remaining key, as indicated by has() returning false for both keys and size being 0.

The clear() method is a fast way to remove a lot of data from a map, but there’s also a way to add a lot of data to a map at one time.

Map Initialization

Also similar to sets, you can initialize a map with data by passing an array to the Map constructor. Each item in the array must itself be an array where the first item is the key and the second is that key’s corresponding value. The entire map, therefore, is an array of these two-item arrays, for example:

  1. let map = new Map([["name", "Nicholas"], ["age", 25]]);
  2. console.log(map.has("name")); // true
  3. console.log(map.get("name")); // "Nicholas"
  4. console.log(map.has("age")); // true
  5. console.log(map.get("age")); // 25
  6. console.log(map.size); // 2

The keys "name" and "age" are added into map through initialization in the constructor. While the array of arrays may look a bit strange, it’s necessary to accurately represent keys, as keys can be any data type. Storing the keys in an array is the only way to ensure they aren’t coerced into another data type before being stored in the map.

The forEach Method on Maps

The forEach() method for maps is similar to forEach() for sets and arrays, in that it accepts a callback function that receives three arguments:

  1. The value from the next position in the map
  2. The key for that value
  3. The map from which the value is read

These callback arguments more closely match the forEach() behavior in arrays, where the first argument is the value and the second is the key (corresponding to a numeric index in arrays). Here’s an example:

  1. let map = new Map([ ["name", "Nicholas"], ["age", 25]]);
  2. map.forEach(function(value, key, ownerMap) {
  3. console.log(key + " " + value);
  4. console.log(ownerMap === map);
  5. });

The forEach() callback function outputs the information that is passed to it. The value and key are output directly, and ownerMap is compared to map to show that the values are equivalent. This outputs:

  1. name Nicholas
  2. true
  3. age 25
  4. true

The callback passed to forEach() receives each key-value pair in the order in which the pairs were inserted into the map. This behavior differs slightly from calling forEach() on arrays, where the callback receives each item in order of numeric index.

I> You can also provide a second argument to forEach() to specify the this value inside the callback function. A call like that behaves the same as the set version of the forEach() method.

Weak Maps

Weak maps are to maps what weak sets are to sets: they’re a way to store weak object references. In weak maps, every key must be an object (an error is thrown if you try to use a non-object key), and those object references are held weakly so they don’t interfere with garbage collection. When there are no references to a weak map key outside a weak map, the key-value pair is removed from the weak map.

The most useful place to employ weak maps is when creating an object related to a particular DOM element in a web page. For example, some JavaScript libraries for web pages maintain one custom object for every DOM element referenced in the library, and that mapping is stored in a cache of objects internally.

The difficult part of this approach is determining when a DOM element no longer exists in the web page, so that the library can remove its associated object. Otherwise, the library would hold onto the DOM element reference past the reference’s usefulness and cause a memory leak. Tracking the DOM elements with a weak map would still allow the library to associate a custom object with every DOM element, and it could automatically destroy any object in the map when that object’s DOM element no longer exists.

I> It’s important to note that only weak map keys, and not weak map values, are weak references. An object stored as a weak map value will prevent garbage collection if all other references are removed.

Using Weak Maps

The ECMAScript 6 WeakMap type is an unordered list of key-value pairs, where a key must be a non-null object and a value can be of any type. The interface for WeakMap is very similar to that of Map in that set() and get() are used to add and retrieve data, respectively:

  1. let map = new WeakMap(),
  2. element = document.querySelector(".element");
  3. map.set(element, "Original");
  4. let value = map.get(element);
  5. console.log(value); // "Original"
  6. // remove the element
  7. element.parentNode.removeChild(element);
  8. element = null;
  9. // the weak map is empty at this point

In this example, one key-value pair is stored. The element key is a DOM element used to store a corresponding string value. That value is then retrieved by passing in the DOM element to the get() method. When the DOM element is later removed from the document and the variable referencing it is set to null, the data is also removed from the weak map.

Similar to weak sets, there is no way to verify that a weak map is empty, because it doesn’t have a size property. Because there are no remaining references to the key, you can’t retrieve the value by calling the get() method, either. The weak map has cut off access to the value for that key, and when the garbage collector runs, the memory occupied by the value will be freed.

Weak Map Initialization

To initialize a weak map, pass an array of arrays to the WeakMap constructor. Just like initializing a regular map, each array inside the containing array should have two items, where the first item is the non-null object key and the second item is the value (any data type). For example:

  1. let key1 = {},
  2. key2 = {},
  3. map = new WeakMap([[key1, "Hello"], [key2, 42]]);
  4. console.log(map.has(key1)); // true
  5. console.log(map.get(key1)); // "Hello"
  6. console.log(map.has(key2)); // true
  7. console.log(map.get(key2)); // 42

The objects key1 and key2 are used as keys in the weak map, and the get() and has() methods can access them. An error is thrown if the WeakMap constructor receives a non-object key in any of the key-value pairs.

Weak Map Methods

Weak maps have only two additional methods available to interact with key-value pairs. There is a has() method to determine if a given key exists in the map and a delete() method to remove a specific key-value pair. There is no clear() method because that would require enumerating keys, and like weak sets, that isn’t possible with weak maps. This example uses both the has() and delete() methods:

  1. let map = new WeakMap(),
  2. element = document.querySelector(".element");
  3. map.set(element, "Original");
  4. console.log(map.has(element)); // true
  5. console.log(map.get(element)); // "Original"
  6. map.delete(element);
  7. console.log(map.has(element)); // false
  8. console.log(map.get(element)); // undefined

Here, a DOM element is once again used as the key in a weak map. The has() method is useful for checking to see if a reference is currently being used as a key in the weak map. Keep in mind that this only works when you have a non-null reference to a key. The key is forcibly removed from the weak map by the delete() method, at which point has() returns false and get() returns undefined.

Private Object Data

While most developers consider the main use case of weak maps to be associated data with DOM elements, there are many other possible uses (and no doubt, some that have yet to be discovered). One practical use of weak maps is to store data that is private to object instances. All object properties are public in ECMAScript 6, and so you need to use some creativity to make data accessible to objects, but not accessible to everything. Consider the following example:

  1. function Person(name) {
  2. this._name = name;
  3. }
  4. Person.prototype.getName = function() {
  5. return this._name;
  6. };

This code uses the common convention of a leading underscore to indicate that a property is considered private and should not be modified outside the object instance. The intent is to use getName() to read this._name and not allow the _name value to change. However, there is nothing standing in the way of someone writing to the _name property, so it can be overwritten either intentionally or accidentally.

In ECMAScript 5, it’s possible to get close to having truly private data, by creating an object using a pattern such as this:

  1. var Person = (function() {
  2. var privateData = {},
  3. privateId = 0;
  4. function Person(name) {
  5. Object.defineProperty(this, "_id", { value: privateId++ });
  6. privateData[this._id] = {
  7. name: name
  8. };
  9. }
  10. Person.prototype.getName = function() {
  11. return privateData[this._id].name;
  12. };
  13. return Person;
  14. }());

This example wraps the definition of Person in an IIFE that contains two private variables, privateData and privateId. The privateData object stores private information for each instance while privateId is used to generate a unique ID for each instance. When the Person constructor is called, a nonenumerable, nonconfigurable, and nonwritable _id property is added.

Then, an entry is made into the privateData object that corresponds to the ID for the object instance; that’s where the name is stored. Later, in the getName() function, the name can be retrieved by using this._id as the key into privateData. Because privateData is not accessible outside of the IIFE, the actual data is safe, even though this._id is exposed publicly.

The big problem with this approach is that the data in privateData never disappears because there is no way to know when an object instance is destroyed; the privateData object will always contain extra data. This problem can be solved by using a weak map instead, as follows:

  1. let Person = (function() {
  2. let privateData = new WeakMap();
  3. function Person(name) {
  4. privateData.set(this, { name: name });
  5. }
  6. Person.prototype.getName = function() {
  7. return privateData.get(this).name;
  8. };
  9. return Person;
  10. }());

This version of the Person example uses a weak map for the private data instead of an object. Because the Person object instance itself can be used as a key, there’s no need to keep track of a separate ID. When the Person constructor is called, a new entry is made into the weak map with a key of this and a value of an object containing private information. In this case, that value is an object containing only name. The getName() function retrieves that private information by passing this to the privateData.get() method, which fetches the value object and accesses the name property. This technique keeps the private information private, and destroys that information whenever an object instance associated with it is destroyed.

Weak Map Uses and Limitations

When deciding whether to use a weak map or a regular map, the primary decision to consider is whether you want to use only object keys. Anytime you’re going to use only object keys, then the best choice is a weak map. That will allow you to optimize memory usage and avoid memory leaks by ensuring that extra data isn’t kept around after it’s no longer accessible.

Keep in mind that weak maps give you very little visibility into their contents, so you can’t use the forEach() method, the size property, or the clear() method to manage the items. If you need some inspection capabilities, then regular maps are a better choice. Just be sure to keep an eye on memory usage.

Of course, if you only want to use non-object keys, then regular maps are your only choice.