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:
let map = new Map();
map.set("title", "Understanding ES6");
map.set("year", 2016);
console.log(map.get("title")); // "Understanding ES6"
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:
let map = new Map(),
key1 = {},
key2 = {};
map.set(key1, 5);
map.set(key2, 42);
console.log(map.get(key1)); // 5
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 mapdelete(key)
- Removes the key and its associated value from the mapclear()
- 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:
let map = new Map();
map.set("name", "Nicholas");
map.set("age", 25);
console.log(map.size); // 2
console.log(map.has("name")); // true
console.log(map.get("name")); // "Nicholas"
console.log(map.has("age")); // true
console.log(map.get("age")); // 25
map.delete("name");
console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.size); // 1
map.clear();
console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.has("age")); // false
console.log(map.get("age")); // undefined
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:
let map = new Map([["name", "Nicholas"], ["age", 25]]);
console.log(map.has("name")); // true
console.log(map.get("name")); // "Nicholas"
console.log(map.has("age")); // true
console.log(map.get("age")); // 25
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:
- The value from the next position in the map
- The key for that value
- 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:
let map = new Map([ ["name", "Nicholas"], ["age", 25]]);
map.forEach(function(value, key, ownerMap) {
console.log(key + " " + value);
console.log(ownerMap === map);
});
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:
name Nicholas
true
age 25
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:
let map = new WeakMap(),
element = document.querySelector(".element");
map.set(element, "Original");
let value = map.get(element);
console.log(value); // "Original"
// remove the element
element.parentNode.removeChild(element);
element = null;
// 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:
let key1 = {},
key2 = {},
map = new WeakMap([[key1, "Hello"], [key2, 42]]);
console.log(map.has(key1)); // true
console.log(map.get(key1)); // "Hello"
console.log(map.has(key2)); // true
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:
let map = new WeakMap(),
element = document.querySelector(".element");
map.set(element, "Original");
console.log(map.has(element)); // true
console.log(map.get(element)); // "Original"
map.delete(element);
console.log(map.has(element)); // false
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:
function Person(name) {
this._name = name;
}
Person.prototype.getName = function() {
return this._name;
};
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:
var Person = (function() {
var privateData = {},
privateId = 0;
function Person(name) {
Object.defineProperty(this, "_id", { value: privateId++ });
privateData[this._id] = {
name: name
};
}
Person.prototype.getName = function() {
return privateData[this._id].name;
};
return Person;
}());
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:
let Person = (function() {
let privateData = new WeakMap();
function Person(name) {
privateData.set(this, { name: name });
}
Person.prototype.getName = function() {
return privateData.get(this).name;
};
return Person;
}());
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.