size and notlength?Among others, the following four data structures are new in ECMAScript 6:Map,WeakMap,Set andWeakSet.
The keys of a Map can be arbitrary values:
> const map = new Map(); // create an empty Map> const KEY = {};> map.set(KEY, 123);> map.get(KEY)123> map.has(KEY)true> map.delete(KEY);true> map.has(KEY)falseYou can use an Array (or any iterable) with [key, value] pairs to set up the initial data in the Map:
constmap=newMap([[1,'one'],[2,'two'],[3,'three'],// trailing comma is ignored]);
A Set is a collection of unique elements:
const arr = [5, 1, 5, 7, 7, 5];const unique = [...new Set(arr)]; // [ 5, 1, 7 ]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:
//----- Manage listenersconst_objToListeners=newWeakMap();functionaddListener(obj,listener){if(!_objToListeners.has(obj)){_objToListeners.set(obj,newSet());}_objToListeners.get(obj).add(listener);}functiontriggerListeners(obj){constlisteners=_objToListeners.get(obj);if(listeners){for(constlisteneroflisteners){listener();}}}//----- Example: attach listeners to an objectconstobj={};addListener(obj,()=>console.log('hello'));addListener(obj,()=>console.log('world'));//----- Example: trigger listenerstriggerListeners(obj);// Output:// hello// world
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 areseveral pitfalls that can trip you up.
TheMap data structure in ECMAScript 6 lets you use arbitrary values as keys and is highly welcome.
Working with single entries:
> const map = new Map();> map.set('foo', 123);> map.get('foo')123> map.has('foo')true> map.delete('foo')true> map.has('foo')falseDetermining the size of a Map and clearing it:
> const map = new Map();> map.set('foo', true);> map.set('bar', false);> map.size2> map.clear();> map.size0You 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):
constmap=newMap([[1,'one'],[2,'two'],[3,'three'],// trailing comma is ignored]);
Alternatively, theset() method is chainable:
constmap=newMap().set(1,'one').set(2,'two').set(3,'three');
Any value can be a key, even an object:
constmap=newMap();constKEY1={};map.set(KEY1,'hello');console.log(map.get(KEY1));// helloconstKEY2={};map.set(KEY2,'world');console.log(map.get(KEY2));// world
Most Map operations need to check whether a value is equal to one of the keys. They do so via the internal operationSameValueZero, which works like===, but considersNaN to be equal to itself.
Let’s first see how=== handlesNaN:
> NaN === NaNfalseConversely, you can useNaN as a key in Maps, just like any other value:
> const map = new Map();> map.set(NaN, 123);> map.get(NaN)123Like===,-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”).
> map.set(-0, 123);> map.get(+0)123Different objects are always considered different. That is something that can’t be configured (yet),as explained later, in the FAQ.
> new Map().set({}, 1).set({}, 2).size2Getting an unknown key producesundefined:
> new Map().get('asfddfsasadf')undefinedLet’s set up a Map to demonstrate how one can iterate over it.
constmap=newMap([[false,'no'],[true,'yes'],]);
Maps record the order in which elements are inserted and honor that order when iterating over keys, values or entries.
keys() returns aniterable over the keys in the Map:
for(constkeyofmap.keys()){console.log(key);}// Output:// false// true
values() returns an iterable over the values in the Map:
for(constvalueofmap.values()){console.log(value);}// Output:// no// yes
entries() returns the entries of the Map as an iterable over [key,value] pairs (Arrays).
for(constentryofmap.entries()){console.log(entry[0],entry[1]);}// Output:// false no// true yes
Destructuring enables you to access the keys and values directly:
for(const[key,value]ofmap.entries()){console.log(key,value);}
The default way of iterating over a Map isentries():
> map[Symbol.iterator] === map.entriestrueThus, you can make the previous code snippet even shorter:
for(const[key,value]ofmap){console.log(key,value);}
The spread operator (...) can turn an iterable into an Array. That lets us convert the result ofMap.prototype.keys() (an iterable) into an Array:
> const map = new Map().set(false, 'no').set(true, 'yes');> [...map.keys()][ false, true ]Maps are also iterable, which means that the spread operator can turn Maps into Arrays:
> const map = new Map().set(false, 'no').set(true, 'yes');> [...map][ [ false, 'no' ], [ true, 'yes' ] ]TheMap methodforEach has the following signature:
Map.prototype.forEach((value,key,map)=>void,thisArg?):void
The signature of the first parameter mirrors the signature of the callback ofArray.prototype.forEach, which is why the value comes first.
constmap=newMap([[false,'no'],[true,'yes'],]);map.forEach((value,key)=>{console.log(key,value);});// Output:// false no// true yes
You canmap() andfilter() Arrays, but there are no such operations for Maps. The solution is:
I’ll use the following Map to demonstrate how that works.
constoriginalMap=newMap().set(1,'a').set(2,'b').set(3,'c');
MappingoriginalMap:
constmappedMap=newMap(// step 3[...originalMap]// step 1.map(([k,v])=>[k*2,'_'+v])// step 2);// Resulting Map: {2 => '_a', 4 => '_b', 6 => '_c'}
FilteringoriginalMap:
constfilteredMap=newMap(// step 3[...originalMap]// step 1.filter(([k,v])=>k<3)// step 2);// Resulting Map: {1 => 'a', 2 => 'b'}
Step 1 is performed by the spread operator (...) whichI 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:
constmap1=newMap().set(1,'a1').set(2,'b1').set(3,'c1');constmap2=newMap().set(2,'b2').set(3,'c2').set(4,'d2');
To combinemap1 andmap2, 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.
> const combinedMap = new Map([...map1, ...map2])> [...combinedMap] // convert to Array to display[ [ 1, 'a1' ], [ 2, 'b2' ], [ 3, 'c2' ], [ 4, 'd2' ] ]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:
>constmyMap=newMap().set(true,7).set({foo:3},['abc']);>[...myMap][[true,7],[{foo:3},['abc']]]
TheMap constructor lets you convert an Array of pairs to a Map:
> new Map([[true, 7], [{foo: 3}, ['abc']]])Map {true => 7, Object {foo: 3} => ['abc']}Let’s use this knowledge to convert any Map with JSON-compatible data to JSON and back:
functionmapToJson(map){returnJSON.stringify([...map]);}functionjsonToMap(jsonStr){returnnewMap(JSON.parse(jsonStr));}
The following interaction demonstrates how these functions are used:
>constmyMap=newMap().set(true,7).set({foo:3},['abc']);>mapToJson(myMap)'[[true,7],[{"foo":3},["abc"]]]'>jsonToMap('[[true,7],[{"foo":3},["abc"]]]')Map{true=>7,Object{foo:3}=>['abc']}
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:
functionstrMapToObj(strMap){constobj=Object.create(null);for(const[k,v]ofstrMap){// We don’t escape the key '__proto__'// which can cause problems on older enginesobj[k]=v;}returnobj;}functionobjToStrMap(obj){conststrMap=newMap();for(constkofObject.keys(obj)){strMap.set(k,obj[k]);}returnstrMap;}
Let’s use these two functions:
> const myMap = new Map().set('yes', true).set('no', false);> strMapToObj(myMap){ yes: true, no: false }> objToStrMap({yes: true, no: false})[ [ 'yes', true ], [ 'no', false ] ]With these helper functions, the conversion to JSON works as follows:
functionstrMapToJson(strMap){returnJSON.stringify(strMapToObj(strMap));}functionjsonToStrMap(jsonStr){returnobjToStrMap(JSON.parse(jsonStr));}
This is an example of using these functions:
> const myMap = new Map().set('yes', true).set('no', false);> strMapToJson(myMap)'{"yes":true,"no":false}'> jsonToStrMap('{"yes":true,"no":false}');Map {'yes' => true, 'no' => false}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:constmap=newMap([[1,'one'],[2,'two'],[3,'three'],// trailing comma is ignored]);
Handling single entries:
Map.prototype.get(key) : anyvalue thatkey is mapped to in this Map. If there is no keykey in this Map,undefined is returned.Map.prototype.set(key, value) : thiskey, it is updated. Otherwise, a new entry is created. This method returnsthis, which means that you can chain it.Map.prototype.has(key) : booleanMap.prototype.delete(key) : booleankey, it is removed andtrue is returned. Otherwise, nothing happens andfalse is returned.Handling all entries:
get Map.prototype.size : numberMap.prototype.clear() : voidIterating 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?) : voidthisArg is provided,this is set to it for each invocation. Otherwise,this is set toundefined.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:
constwm=newWeakMap()wm.set('abc',123);// TypeErrorwm.set({},123);// OK
The keys in a WeakMap areweakly 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. QuotingMark Miller: “The mapping from weakmap/key pair value can only be observed or affected by someone who has both the weakmap and the key. Withclear(), 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 functioncountOwnKeys is an example: it caches previous results in the WeakMapcache.
constcache=newWeakMap();functioncountOwnKeys(obj){if(cache.has(obj)){console.log('Cached');returncache.get(obj);}else{console.log('Computed');constcount=Object.keys(obj).length;cache.set(obj,count);returncount;}}
If we use this function with an objectobj, you can see that the result is only computed for the first invocation, while a cached value is used for the second invocation:
>constobj={foo:1,bar:2};>countOwnKeys(obj)Computed2>countOwnKeys(obj)Cached2
Let’s say we want to attach listeners to objects without changing the objects. You’d be able to add listeners to an objectobj:
constobj={};addListener(obj,()=>console.log('hello'));addListener(obj,()=>console.log('world'));
And you’d be able to trigger the listeners:
triggerListeners(obj);// Output:// hello// world
The two functionsaddListener() andtriggerListeners() can be implemented as follows.
const_objToListeners=newWeakMap();functionaddListener(obj,listener){if(!_objToListeners.has(obj)){_objToListeners.set(obj,newSet());}_objToListeners.get(obj).add(listener);}functiontriggerListeners(obj){constlisteners=_objToListeners.get(obj);if(listeners){for(constlisteneroflisteners){listener();}}}
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 ofCountdown:
const_counter=newWeakMap();const_action=newWeakMap();classCountdown{constructor(counter,action){_counter.set(this,counter);_action.set(this,action);}dec(){letcounter=_counter.get(this);if(counter<1)return;counter--;_counter.set(this,counter);if(counter===0){_action.get(this)();}}}
More information on this technique is given inthe chapter on classes.
The constructor and the four methods ofWeakMap work the same as theirMap equivalents:
newWeakMap(entries?:Iterable<[any,any]>)WeakMap.prototype.get(key):anyWeakMap.prototype.set(key,value):thisWeakMap.prototype.has(key):booleanWeakMap.prototype.delete(key):boolean
ECMAScript 5 doesn’t have a Set data structure, either. There are two possible work-arounds:
indexOf(), remove elements viafilter(), etc. This is not a very fast solution, but it’s easy to implement. One issue to be aware of is thatindexOf() can’t find the valueNaN.ECMAScript 6 has the data structureSet which works for arbitrary values, is fast and handlesNaN correctly.
Managing single elements:
> const set = new Set();> set.add('red')> set.has('red')true> set.delete('red')true> set.has('red')falseDetermining the size of a Set and clearing it:
> const set = new Set();> set.add('red')> set.add('green')> set.size2> set.clear();> set.size0You can set up a Set via an iterable over the elements that make up the Set. For example, via an Array:
constset=newSet(['red','green','blue']);
Alternatively, theadd method is chainable:
constset=newSet().add('red').add('green').add('blue');
new Set() has at most one argumentTheSet constructor has zero or one arguments:
Further arguments are ignored, which may lead to unexpected results:
> Array.from(new Set(['foo', 'bar']))[ 'foo', 'bar' ]> Array.from(new Set('foo', 'bar'))[ 'f', 'o' ]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 ofNaN being like any other value.
> const set = new Set([NaN]);> set.size1> set.has(NaN)trueAdding an element a second time has no effect:
> const set = new Set();> set.add('foo');> set.size1> set.add('foo');> set.size1Similarly to===, two different objects are never considered equal (which can’t currently be customized,as explained later, in the FAQ, later):
> const set = new Set();> set.add({});> set.size1> set.add({});> set.size2Sets are iterable and thefor-of loop works as you’d expect:
constset=newSet(['red','green','blue']);for(constxofset){console.log(x);}// Output:// red// green// blue
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:
constset=newSet(['red','green','blue']);constarr=[...set];// ['red', 'green', 'blue']
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:
constarr=[3,5,2,2,5,5];constunique=[...newSet(arr)];// [3, 5, 2]
In contrast to Arrays, Sets don’t have the methodsmap() andfilter(). A work-around is to convert them to Arrays and back.
Mapping:
constset=newSet([1,2,3]);set=newSet([...set].map(x=>x*2));// Resulting Set: {2, 4, 6}
Filtering:
constset=newSet([1,2,3,4,5]);set=newSet([...set].filter(x=>(x%2)==0));// Resulting Set: {2, 4}
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 Seta and Setb.
consta=newSet([1,2,3]);constb=newSet([4,3,2]);constunion=newSet([...a,...b]);// {1,2,3,4}
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 thata andb are converted to Arrays and concatenated. It is equivalent to[...a].concat([...b]).
Intersection (a ∩b): create a Set that contains those elements of Seta that are also in Setb.
consta=newSet([1,2,3]);constb=newSet([4,3,2]);constintersection=newSet([...a].filter(x=>b.has(x)));// {2,3}
Steps: Converta to an Array, filter the elements, convert the result to a Set.
Difference (a \b): create a Set that contains those elements of Seta that are not in Setb. This operation is also sometimes calledminus (-).
consta=newSet([1,2,3]);constb=newSet([4,3,2]);constdifference=newSet([...a].filter(x=>!b.has(x)));// {1}
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:constset=newSet(['red','green','blue']);
Single Set elements:
Set.prototype.add(value) : thisvalue to this Set. This method returnsthis, which means that it can be chained.Set.prototype.has(value) : booleanvalue is in this Set.Set.prototype.delete(value) : booleanvalue from this Set.All Set elements:
get Set.prototype.size : numberSet.prototype.clear() : voidIterating 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 andkey are both set to the element, so that this method works similarly toMap.prototype.forEach. IfthisArg is provided,this is set to it for each call. Otherwise,this is set toundefined.Symmetry withMap: 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:
constset=newSet(['a','b','c']);constmap=newMap(set.entries());// Map { 'a' => 'a', 'b' => 'b', 'c' => 'c' }
AWeakSet is a Set that doesn’t prevent its elements from being garbage-collected. Consult the section onWeakMap 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:
const_proxies=newWeakSet();functioncreateProxy(obj){constproxy=···;_proxies.add(proxy);returnproxy;}functionisProxy(obj){return_proxies.has(obj);}
The complete example is shown inthe 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 classFoo can ensure that its methods are only applied to instances that were created by it:
constfoos=newWeakSet();classFoo{constructor(){foos.add(this);}method(){if(!foos.has(this)){thrownewTypeError('Incompatible object!');}}}
The constructor and the three methods ofWeakSet work the same as theirSet equivalents:
newWeakSet(elements?:Iterable<any>)WeakSet.prototype.add(value)WeakSet.prototype.has(value)WeakSet.prototype.delete(value)
size and notlength?Arrays have the propertylength to count the number of entries. Maps and Sets have a different property,size.
The reason for this difference is thatlength 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 theOr operator (||), as demonstrated in the following code.countChars returns a Map that maps characters to numbers of occurrences.
functioncountChars(chars){constcharCounts=newMap();for(constchofchars){ch=ch.toLowerCase();constprevCount=charCounts.get(ch)||0;// (A)charCounts.set(ch,prevCount+1);}returncharCounts;}
In line A, the default0 is used ifch is not in thecharCounts andget() returnsundefined.
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.keymap.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.