size
and not length
?Among others, the following four data structures are new in ECMAScript 6: Map
, WeakMap
, Set
and WeakSet
.
The keys of a Map can be arbitrary values:
You can use an Array (or any iterable) with [key, value] pairs to set up the initial data in the Map:
A Set is a collection of unique elements:
As you can see, you can initialize a Set with elements if you hand the constructor an iterable (arr
in the example) over those elements.
A WeakMap is a Map that doesn’t prevent its keys from being garbage-collected. That means that you can associate data with objects without having to worry about memory leaks. For example:
JavaScript has always had a very spartan standard library. Sorely missing was a data structure for mapping values to values. The best you can get in ECMAScript 5 is a Map from strings to arbitrary values, by abusing objects. Even then there are several pitfalls that can trip you up.
The Map
data structure in ECMAScript 6 lets you use arbitrary values as keys and is highly welcome.
Working with single entries:
Determining the size of a Map and clearing it:
You can set up a Map via an iterable over key-value “pairs” (Arrays with 2 elements). One possibility is to use an Array (which is iterable):
Alternatively, the set()
method is chainable:
Any value can be a key, even an object:
Most Map operations need to check whether a value is equal to one of the keys. They do so via the internal operation SameValueZero, which works like ===
, but considers NaN
to be equal to itself.
Let’s first see how ===
handles NaN
:
Conversely, you can use NaN
as a key in Maps, just like any other value:
Like ===
, -0
and +0
are considered the same value. That is normally the best way to handle the two zeros (details are explained in “Speaking JavaScript”).
Different objects are always considered different. That is something that can’t be configured (yet), as explained later, in the FAQ.
Getting an unknown key produces undefined
:
Let’s set up a Map to demonstrate how one can iterate over it.
Maps record the order in which elements are inserted and honor that order when iterating over keys, values or entries.
keys()
returns an iterable over the keys in the Map:
values()
returns an iterable over the values in the Map:
entries()
returns the entries of the Map as an iterable over [key,value] pairs (Arrays).
Destructuring enables you to access the keys and values directly:
The default way of iterating over a Map is entries()
:
Thus, you can make the previous code snippet even shorter:
The spread operator (...
) can turn an iterable into an Array. That lets us convert the result of Map.prototype.keys()
(an iterable) into an Array:
Maps are also iterable, which means that the spread operator can turn Maps into Arrays:
The Map
method forEach
has the following signature:
The signature of the first parameter mirrors the signature of the callback of Array.prototype.forEach
, which is why the value comes first.
You can map()
and filter()
Arrays, but there are no such operations for Maps. The solution is:
I’ll use the following Map to demonstrate how that works.
Mapping originalMap
:
Filtering originalMap
:
Step 1 is performed by the spread operator (...
) which I have explained previously.
There are no methods for combining Maps, which is why the approach from the previous section must be used to do so.
Let’s combine the following two Maps:
To combine map1
and map2
, I turn them into Arrays via the spread operator (...
) and concatenate those Arrays. Afterwards, I convert the result back to a Map. All of that is done in the first line.
If a Map contains arbitrary (JSON-compatible) data, we can convert it to JSON by encoding it as an Array of key-value pairs (2-element Arrays). Let’s examine first how to achieve that encoding.
The spread operator lets you convert a Map to an Array of pairs:
The Map
constructor lets you convert an Array of pairs to a Map:
Let’s use this knowledge to convert any Map with JSON-compatible data to JSON and back:
The following interaction demonstrates how these functions are used:
Whenever a Map only has strings as keys, you can convert it to JSON by encoding it as an object. Let’s examine first how to achieve that encoding.
The following two function convert string Maps to and from objects:
Let’s use these two functions:
With these helper functions, the conversion to JSON works as follows:
This is an example of using these functions:
Constructor:
new Map(entries? : Iterable<[any,any]>)
iterable
then an empty Map is created. If you do provide an iterable over [key, value] pairs then those pairs are used to add entries to the Map. For example:
Handling single entries:
Map.prototype.get(key) : any
value
that key
is mapped to in this Map. If there is no key key
in this Map, undefined
is returned.Map.prototype.set(key, value) : this
key
, it is updated. Otherwise, a new entry is created. This method returns this
, which means that you can chain it.Map.prototype.has(key) : boolean
Map.prototype.delete(key) : boolean
key
, it is removed and true
is returned. Otherwise, nothing happens and false
is returned.Handling all entries:
get Map.prototype.size : number
Map.prototype.clear() : void
Iterating and looping: happens in the order in which entries were added to a Map.
Map.prototype.entries() : Iterable<[any,any]>
Map.prototype.forEach((value, key, collection) => void, thisArg?) : void
thisArg
is provided, this
is set to it for each invocation. Otherwise, this
is set to undefined
.Map.prototype.keys() : Iterable<any>
Map.prototype.values() : Iterable<any>
Map.prototype[Symbol.iterator]() : Iterable<[any,any]>
Map.prototype.entries
.WeakMaps work mostly like Maps, with the following differences:
The following sections explain each of these differences.
If you add an entry to a WeakMap then the key must be an object:
The keys in a WeakMap are weakly held: Normally, an object that isn’t referred to by any storage location (variable, property, etc.) can be garbage-collected. WeakMap keys do not count as storage locations in that sense. In other words: an object being a key in a WeakMap does not prevent the object being garbage-collected.
Additionally, once a key is gone, its entry will also disappear (eventually, but there is no way to detect when, anyway).
It is impossible to inspect the innards of a WeakMap, to get an overview of them. That includes not being able to iterate over keys, values or entries. Put differently: to get content out of a WeakMap, you need a key. There is no way to clear a WeakMap, either (as a work-around, you can create a completely new instance).
These restrictions enable a security property. Quoting Mark Miller: “The mapping from weakmap/key pair value can only be observed or affected by someone who has both the weakmap and the key. With clear()
, someone with only the WeakMap would’ve been able to affect the WeakMap-and-key-to-value mapping.”
Additionally, iteration would be difficult to implement, because you’d have to guarantee that keys remain weakly held.
WeakMaps are useful for associating data with objects whose life cycle you can’t (or don’t want to) control. In this section, we look at two examples:
With WeakMaps, you can associate previously computed results with objects, without having to worry about memory management. The following function countOwnKeys
is an example: it caches previous results in the WeakMap cache
.
If we use this function with an object obj
, you can see that the result is only computed for the first invocation, while a cached value is used for the second invocation:
Let’s say we want to attach listeners to objects without changing the objects. You’d be able to add listeners to an object obj
:
And you’d be able to trigger the listeners:
The two functions addListener()
and triggerListeners()
can be implemented as follows.
The advantage of using a WeakMap here is that, once an object is garbage-collected, its listeners will be garbage-collected, too. In other words: there won’t be any memory leaks.
In the following code, the WeakMaps _counter
and _action
are used to store the data of virtual properties of instances of Countdown
:
More information on this technique is given in the chapter on classes.
The constructor and the four methods of WeakMap
work the same as their Map
equivalents:
ECMAScript 5 doesn’t have a Set data structure, either. There are two possible work-arounds:
indexOf()
, remove elements via filter()
, etc. This is not a very fast solution, but it’s easy to implement. One issue to be aware of is that indexOf()
can’t find the value NaN
.ECMAScript 6 has the data structure Set
which works for arbitrary values, is fast and handles NaN
correctly.
Managing single elements:
Determining the size of a Set and clearing it:
You can set up a Set via an iterable over the elements that make up the Set. For example, via an Array:
Alternatively, the add
method is chainable:
new Set()
has at most one argument The Set
constructor has zero or one arguments:
Further arguments are ignored, which may lead to unexpected results:
For the second Set, only 'foo'
is used (which is iterable) to define the Set.
As with Maps, elements are compared similarly to ===
, with the exception of NaN
being like any other value.
Adding an element a second time has no effect:
Similarly to ===
, two different objects are never considered equal (which can’t currently be customized, as explained later, in the FAQ, later):
Sets are iterable and the for-of
loop works as you’d expect:
As you can see, Sets preserve iteration order. That is, elements are always iterated over in the order in which they were inserted.
The previously explained spread operator (...
) works with iterables and thus lets you convert a Set to an Array:
We now have a concise way to convert an Array to a Set and back, which has the effect of eliminating duplicates from the Array:
In contrast to Arrays, Sets don’t have the methods map()
and filter()
. A work-around is to convert them to Arrays and back.
Mapping:
Filtering:
ECMAScript 6 Sets have no methods for computing the union (e.g. addAll
), intersection (e.g. retainAll
) or difference (e.g. removeAll
). This section explains how to work around that limitation.
Union (a
∪ b
): create a Set that contains the elements of both Set a
and Set b
.
The pattern is always the same:
The spread operator (...
) inserts the elements of something iterable (such as a Set) into an Array. Therefore, [...a, ...b]
means that a
and b
are converted to Arrays and concatenated. It is equivalent to [...a].concat([...b])
.
Intersection (a
∩ b
): create a Set that contains those elements of Set a
that are also in Set b
.
Steps: Convert a
to an Array, filter the elements, convert the result to a Set.
Difference (a
\ b
): create a Set that contains those elements of Set a
that are not in Set b
. This operation is also sometimes called minus (-
).
Constructor:
new Set(elements? : Iterable<any>)
iterable
then an empty Set is created. If you do then the iterated values are added as elements to the Set. For example:
Single Set elements:
Set.prototype.add(value) : this
value
to this Set. This method returns this
, which means that it can be chained.Set.prototype.has(value) : boolean
value
is in this Set.Set.prototype.delete(value) : boolean
value
from this Set.All Set elements:
get Set.prototype.size : number
Set.prototype.clear() : void
Iterating and looping:
Set.prototype.values() : Iterable<any>
Set.prototype[Symbol.iterator]() : Iterable<any>
Set.prototype.values
.Set.prototype.forEach((value, key, collection) => void, thisArg?)
value
and key
are both set to the element, so that this method works similarly to Map.prototype.forEach
. If thisArg
is provided, this
is set to it for each call. Otherwise, this
is set to undefined
.Symmetry with Map
: The following two methods only exist so that the interface of Sets is similar to the interface of Maps. Each Set element is handled as if it were a Map entry whose key and value are the element.
Set.prototype.entries() : Iterable<[any,any]>
Set.prototype.keys() : Iterable<any>
entries()
allows you to convert a Set to a Map:
A WeakSet
is a Set that doesn’t prevent its elements from being garbage-collected. Consult the section on WeakMap
for an explanation of why WeakSets don’t allow iteration, looping and clearing.
Given that you can’t iterate over their elements, there are not that many use cases for WeakSets. They do enable you to mark objects.
For example, if you have a factory function for proxies, you can use a WeakSet to record which objects were created by that factory:
The complete example is shown in the chapter on proxies.
_proxies
must be a WeakSet, because a normal Set would prevent a proxy from being garbage-collected once it isn’t referred to, anymore.
Domenic Denicola shows how a class Foo
can ensure that its methods are only applied to instances that were created by it:
The constructor and the three methods of WeakSet
work the same as their Set
equivalents:
size
and not length
? Arrays have the property length
to count the number of entries. Maps and Sets have a different property, size
.
The reason for this difference is that length
is for sequences, data structures that are indexable – like Arrays. size
is for collections that are primarily unordered – like Maps and Sets.
It would be nice if there were a way to configure what Map keys and what Set elements are considered equal. But that feature has been postponed, as it is difficult to implement properly and efficiently.
If you use a key to get something out of a Map, you’d occasionally like to specify a default value that is returned if the key is not in the Map. ES6 Maps don’t let you do this directly. But you can use the Or
operator (||
), as demonstrated in the following code. countChars
returns a Map that maps characters to numbers of occurrences.
In line A, the default 0
is used if ch
is not in the charCounts
and get()
returns undefined
.
If you map anything other than strings to any kind of data, you have no choice: you must use a Map.
If, however, you are mapping strings to arbitrary data, you must decide whether or not to use an object. A rough general guideline is:
obj.key
map.get(theKey)
Map keys mainly make sense if they are compared by value (the same “content” means that two values are considered equal, not the same identity). That excludes objects. There is one use case – externally attaching data to objects, but that use case is better served by WeakMaps where an entry goes away when the key disappears.