Maps, Sets and Iterators in JavaScript


In my last JavaScript post I wrote about about how to make advanced usage of objects in JavaScript. Continuing in a similar direction, in this post I want look at some new kinds of objects in JavaScript. I will look at maps and sets, including their “weak” counterparts, weakmaps and weaksets, also the new for...of iterator and the new type Array Iterator.

As of September 2014, these new features are not fully implemented across all browsers yet, so most of these examples will only work in Firefox and Chrome Canary. I am using Firefox Nightly and Chrome Canary to create and test the jsfiddle examples. It may be the case that the examples also work in some of the beta versions, but I haven’t checked.

I am going to begin by looking at the new Map type in ECMAScript6.

 Maps

Developers usually just use regular JavaScript objects when they want maps. Stuff is mapped in an object by using strings as a key and this is possible with any kind of Object in Javascript.

var obj = {};
obj.foo = "bar";
console.log(obj.foo); //logs "bar"

In ECMAScript 6, the new type Map can be used very similar to a regular object.

var map = new Map();
map.set("foo", "bar");
console.log(map.get("foo")); //logs "bar"

jsfiddle (created and tested with Firefox Nightly)

You will immediately notice that a map isn’t created with the literal object syntax, and that one uses set and get methods to store and access data. Map constructors can also take an argument, something we’ll look at that later. Since maps are just objects it is possible to store arbitrary data on a map using the . notation but then you are not going to get to make use of the benefits of maps and you probably never want to do such a thing with a Map type. Map instances can be queried and manipulated with more than just set and get.

var animalSounds = new Map();

animalSounds.set("dog", "woof");
animalSounds.set("cat", "meow");
animalSounds.set("frog", "ribbit");

console.log(animalSounds.size); //logs 3
console.log(animalSounds.has("dog")); //logs true

animalSounds.delete("dog");

console.log(animalSounds.size); //logs 2
console.log(animalSounds.has("dog")); //logs false

animalSounds.clear();
console.log(animalSounds.size); //logs 0

jsfiddle (created and tested with Firefox Nightly)

In this example you can see that the size property on a map instance gives you the count of keys and values stored in a Map. If you were using regular Objects you’d have to keep track of the number of things in the Object yourself or iterate and count. With map this isn’t necessary. Also available is a has method to test if a key is stored in the map, a delete method to delete a key from a map and a clear to clear all keys and values in a map.

The Map type, unlike regular objects in JavaScript, allow you to use any type as a key to refer to data, not just strings. This is a big improvement, especially if you have been storing data in objects using numbers. Consider the following:

var userId, usersObj, usersMap;

usersObj = {
    1: "sally",
    2: "bob",
    3: "jane"
};


console.log(usersObj[1]); //logs "sally", toString called on 1

for (userId in usersObj) {
   console.log(userId, typeof userId); //logs 1..3, "string"
   if (userId === 1) {
     console.log("This is never logged because userId is a string.");
   }
}

With objects, developers have been able to use numbers as property names, or keys as it were, but when this is done the number is actually being turned into a string.

console.log(usersObj[1]); //logs "sally", toString called on 1
"sally"

A number as a key actually works here because toString is being called on the number given to the object. However when you iterate over usersObj it becomes clear that something is amiss.

for (userId in usersObj) {
   console.log(userId, typeof userId); //logs 1..3, "string"
   if (userId === 1) {
     console.log("This is never logged because userId is a string.");
   }
}

Here we see userId is a string and the console.log statement inside the if statement is never executed because we mistakenly expected userId to be a number.

"1" "string"
"2" "string"
"3" "string"

If you have been using numbers as primary keys, as many databases will do by default, you can run into trouble if you want to iterate over your JavaScript object to look for an id. You will not find your id unless you know that the key for an Object is always of type string. Numbers are simply being converted to strings.

With maps however, the type of the key stays whatever it was when you set it.

usersMap = new Map();
usersMap.set(1, "sally");
usersMap.set(2, "bob");
usersMap.set(3, "jane");

console.log(usersMap.get(1)); //logs "sally"
usersMap.forEach(function (username, userId) {
  console.log(userId, typeof userId); //logs 1..3, "number"
  if (userId === 1) {
     console.log("We found sally.");
  }
});

jsfiddle (created and tested with Firefox Nightly)

1 "number"
"We found sally."
2 "number"
3 "number"

The number stays as type number. You never need convert your id to a string or convert the Object’s key to a number when you’re using a map.

In addition to strings and numbers, you can use other primitive types as keys. Here is an example using a boolean as a key.

var map;

map = new Map();
map.set(true, [
    "1 + 1 = 2",
    "cows are animals",
    "grass is green",
]);
map.set(false, [
    "the moon is made from cheese",
    "the earth is flat",
    "pyramids were made by aliens",
]);


map.forEach(function (value, key) {
    console.log(typeof key, key);
});

jsfiddle (created and tested with Firefox Nightly)

This logs:

"boolean" true
"boolean" false

You can even use Objects as a key, here is an example of such a map:

var obj, map;

map = new Map();
obj = {foo: "bar"};

map.set(obj, "foobar");

obj.newProp = "stuff";

console.log(map.has(obj)); //logs true

console.log(map.get(obj)); //logs "foobar"

jsfiddle (created and tested with Firefox Nightly)

Even NaN can be a key.

var map, errors, result;
errors = new Map();
errors.set(NaN, "That was not a number!");
errors.set(Infinity, "That was infinity!");
[
    1 + 2,
    1/0,
    1/"foo",
].forEach(function (result) {
    if (errors.has(result)) {
        console.log(errors.get(result));
    } else {
        console.log("The result was", result);
    }
});

jsfiddle (created and tested with Firefox Nightly)

Result:

"The result was" 3 
"That was infinity!"
"That was not a number!"

Even though NaN !== NaN you can use NaN this way with Maps.

var errors;
errors = new Map();
errors.set(NaN, "Not a number!");

console.log(NaN === NaN); //logs false
console.log(errors.has(NaN)); //logs true

jsfiddle (created and tested with Firefox Nightly)

It is totally possible to mix and match types in a map, here is a pretty silly example:

var map = new Map();

map.set(1, "one");
map.set("two", 2);
map.set(true, false);
map.set({foo: "bar"}, ["foo", "bar"]);

console.log(map.size); //logs "4"

jsfiddle (created and tested with Firefox Nightly)

There are multiple ways to iterate over a Map, some of which we will look at in just a bit, but a very straight forward way is to use a map instance’s forEach method.

var map = new Map();

map.set(1, "one");
map.set("two", 2);
map.set(true, false);
map.set({foo: "bar"}, ["foo", "bar"]);

map.forEach(function (value, key, mapObj) {
    console.log("value:", value, "key:", key, "map", mapObj === map);
});

jsfiddle

Result:

"value:" "one" "key:" 1 "map" true
"value:" 2 "key:" "two" "map" true
"value:" false "key:" true "map" true
"value:" Array [ "foo", "bar" ] "key:" Object { foo: "bar" } "map" true

Much like the Array.prototype.forEach, a map’s forEach is given a function that is called for each value set on the map. This function is given three parameters: the value, they key, and a reference to the map object itself as the final argument.


 for…of

We are not just yet done talking about maps, but we need to diverge for a bit because before I can go on I need to introduce iterators. ECMAScript 6 introduces some new ways to iterate over data and I will only mention two of them in this post, for...of and the new Array Iterator type.

for...of is a new type of iteration statement that differs from for...in in that it iterates over the values instead of indexes or keys, and that it can be used with maps and sets. Before we switch back to maps let’s take a look at iterating over arrays.

var i, value, a;

a = ["a", "b", "c"];
a.foo = "bar";
for (i in a) {
    console.log(i);
}


for (value of a) {
    console.log(value);
}

jsfiddle (created and tested with Firefox Nightly)

Usually you would iterate over an array with a for(i = 0; i < a.length; i++) or use its forEach but you could also iterate via for...in but this example demonstrates a problem with that as for...in also iterates over properties of the array object.

a = ["a", "b", "c"];
a.foo = "bar";
for (i in a) {
    console.log(i);
}
0
1
2
foo

Notice the “foo”. Instead of iterating over the indexes and property names, for...of iterates over its values.

for (value of a) {
    console.log(value);
}
a
b
c

Notice the lack of “foo”. Only array values are iterated over, not object values. As a quick aside I should also note that my experience has been that I have found for...in to be considerably slower in the past than the available alternatives. I will avoid looking at the performance of for...of until it has made it into stable versions of Chrome and other browsers.

You cannot use for...of with just any object, it will result in a JavaScript error.

var value, obj = {foo1: "bar1", foo2: "bar2"};

//This will result in a JavaScript error:
for (value of obj) {
    console.log(value);
}

jsfiddle (created and tested with Firefox Nightly)

You can use for...of on arrays, array like types like arguments and node lists, generators, maps and sets. Sets we’ll take a look later and generators are beyond the scope of this blog post. Here is for...of with a map:

var data, map = new Map();

map.set("dog", "woof");
map.set("cow", "moo");

for (data of map) {
    console.log(data);
}

jsfiddle (created and tested with Firefox Nightly)

Each iteration of a map with for...of is given an array with two values.

Array [ "dog", "woof" ]
Array [ "cow", "moo" ]

Each pair has the key at index 0 and its value at index 1.

I find maps to be preferable to Objects for several reasons, but not the least is the simple iteration. for...of is superior than a for...in as object values are not iterated over, no more need for checking hasOwnProperty, and you get the index and the value. When to use for...of or the maps forEach depends what is simpler., for...of avoids the extra function call for each iteration, avoids the need to binding a thisArg and is simply easier to look at.


 Array Iterator

Continuing on, the other new method of iteration is the Array Iterator type which is also applicable to maps, sets and arrays. An array iterator is an object that has a next method which when called returns an object with value and done properties. The next method is used to iterate, the value of the returned object is the value obviously, and done will tell you when iteration has finished. There are new array and map methods that return array iterators.

function logIterator(iterator) {
    var current;
    while(true) {
        current = iterator.next();
        if (current.done) {
            break;
        }
        console.log(current.value);
    }
}

logIterator(["a","b", "c", "d"].entries());
logIterator(["a","b", "c", "d"].keys());


var map = new Map();
map.set(0, "a");
map.set(1, "b");
map.set(2, "c");
map.set(3, "d");
logIterator(map.entries());
logIterator(map.keys());
logIterator(map.values());

jsfiddle (created and tested with Firefox Nightly)

The function logIterator takes an Array Iterator as an argument and iterates over it with a while loop. It calls next to iterate and logs the return value. It checks done to break out of the while.

Here we use the array’s entries and keys methods to get Array Iterators and log their values.

logIterator(["a","b", "c", "d"].entries());
logIterator(["a","b", "c", "d"].keys());

Result:

[0, "a"]
[1, "b"]
[2, "c"]
[3, "d"]
0
1
2
3

The entries method returns an Array Iterator the value of which will be an Array pair, much like you get for a for...of when iterating over a map. At index 0 you get the current iteration’s index, at index 1 you get the value for the current iteration.

The keys method creates an Array Iterator that simply returns an index, or key as it were, for the Array at each iteration.

The map methods entries and keys behaves like an Array, the additional values method iterates over the map’s values:

var map = new Map();
map.set(0, "a");
map.set(1, "b");
map.set(2, "c");
map.set(3, "d");
logIterator(map.entries());
logIterator(map.keys());
logIterator(map.values());

Result:

[0, "a"]
[1, "b"]
[2, "c"]
[3, "d"]
0
1
2
3
"a"
"b"
"c"
"d"

Very much the same output. Note the Map returned data in the order it was given but there is absolutely nothing said in the specification of Maps being sorted nor of any order guarantee. If you want that, use an Array or a Set.

Unless you are doing something special with an Array Iterator you don’t need to manually write a function that calls next and checks done. You can just use for...of:

var map = new Map();
map.set(0, "a");
map.set(1, "b");
map.set(2, "c");
map.set(3, "d");

for (data of map.entries()) {
    console.log(data);
}

jsfiddle

This prints exactly as what it did when we used logIterator before.

Now that we know what Array Iterator is, we can look at the constructor arguments for maps. You can create a map based of an Array Iterator.

var map = new Map(["a","b", "c", "d"].entries());

console.log(map); //logs Map { 0: "a", 1: "b", 2: "c", 3: "d" }
console.log(map.get(0)); //logs "a"

jsfiddle (created and tested with Firefox Nightly)

You can also just use arrays of pairs:

var map = new Map([["dog", "woof"],["cow", "moo"]]);

console.log(map); //logs Map { dog: "woof", cow: "moo" }
console.log(map.get("dog")); //logs "woof"

jsfiddle (created and tested with Firefox Nightly)

Duplicate keys in this case seem to override previous values in both Chrome Canary and Firefox Nightly.

var map = new Map([["dog", "woof"],["cow", "moo"], ["dog", "ribbit"]]);

console.log(map); //logs Map { dog: "ribbit", cow: "moo" }
console.log(map.get("dog")); //logs "ribbit"

jsfiddle (created and tested with Firefox Nightly)


 Weakmaps

weakmap.png

WeakMaps are much like Maps except for some important differences. The first important difference is that you can only use objects as keys for a WeakMap. You cannot use numbers, strings or other primitive types as keys. The second difference is that these keys are weakly held. Objects used as keys by a WeakMap can be garbage collected if there are no other references to the object elsewhere in your application. Another important difference is that WeakMaps only have a subset of the methods maps have and cannot be iterated over in a for...of loop.

var data, wmap = new WeakMap();
wmap.set({foo: "bar"}, "foobar");

//This is a JavaScript error:
for (data of wmap) {
    console.log(data);
}

jsfiddle

The methods a WeakMap has are limited, to get, set, has, delete and clear.

One use case for a WeakMap could be to store metadata for an object. You do not have to manually clean up a WeakMap’s references when you want to remove references to objects. For example, in the browser it is possible to create unintentional memory leaks by keeping references to HTML elements around in your JavaScript. Even though you may have removed HTML elements from the DOM, if you still have references to them somewhere in your JavaScript they may not be garbage collected. If you use a WeakMap you don’t have to worry about this. I am not saying that maintaining state in the DOM is a good idea, in fact I think it is probably a bad idea that I have seen lead to a lot of problems, but you could do it with a WeakMap and not have it lead to memory leaks. A great example for a good use case might be an SVG based line chart. You could store the details for a point in a WeakMap to show in a tooltip. When the user hovers over the point, the tooltip uses the SVG element hovered over to query the WeakMap for information for the tooltip. When the line chart updates and the point goes away, the reference in the WeakMap should be cleared up and not present a problem.

For the purpose of this blog post I will use a less ideal example. I will use a shopping cart that maintains state using elements. Again, this is only for demonstration purposes, it is probably best to store state not using elements.

As of September 2014 the following example only works in Firefox Nightly, only because I used ES6 string templates, sorry.

var prices, shoppingCart = new WeakMap();

prices = new Map([
    ["smartphone", new Map([
        ["iPhone6", 610],
        ["galaxyS5", 650],
        ["motoX", 179],
    ])],
    ["accessory", new Map([
        ["stylus", 20],
        ["case", 40],
    ])],
]);


function getProductFromShelf(type) {
    var productEl, clone, price;
    productEl = document.getElementById("shelf").querySelector(`.${type}`);
    price = prices.get(productEl.classList[1]).get(type);
    clone = productEl.cloneNode(true);
    document.getElementById("shoppingCart").appendChild(clone);
    shoppingCart.set(clone, price);
}

function printTotal() {
     var products, total = 0;
     products = document.getElementById("shoppingCart")
         .querySelectorAll("div");
     for (product of products) {
         total += shoppingCart.get(product);
     }
     document.getElementById("total").textContent = `$${total}`;
}

getProductFromShelf("iPhone6");
getProductFromShelf("motoX");
getProductFromShelf("case");

printTotal();

jsfiddle (works in Firefox Nightly only)

This code clones HTML elements from the shelf and puts them into the shopping cart HTML element. When this is done an instance of WeakMap, shoppingCart, maintains metadata associated with the product put into the shopping cart. When this code wants to calculate the total it iterates over all the elements in the shopping cart and grabs the products associated price in shoppingCart and adds it up. In this example the total ends up being $829. When we want to remove items from the shopping cart, we need only remove the element from the DOM, we don’t have to remove any metadata from shoppingCart.

As a quick note I made use of ECMAScript 6 string templates:

    productEl = document.getElementById("shelf").querySelector(`.${type}`);

Note the:

`.${type}`

This is new in Firefox Nightly and I wanted to try it out. It references the type variable so this string will become ".iphone6" or ".motoX" for example, depending on the value in type. I don’t have to use the ugly string concatenation + operator to make hard to read strings.


 Sets

New in JavaScript is the Set type which is a unique list of values enumerable in insertion order. Unlike Python’s set it does not provide set operation functionality like finding differences, intersections and unions. It’s simply a unique list.

var set = new Set(["a", "a","e", "b", "c", "b", "b", "b", "d"]);
console.log(set);

jsfiddle

Result:

Set [ "a", "e", "b", "c", "d" ]

And when iterated over it maintains insertion order:

var value, set = new Set(["a", "a","e", "b", "c", "b", "b", "b", "d"]);

for (value of set) {
    console.log(value);
}

jsfiddle

"a"
"e"
"b"
"c"
"d"

Sets can’t be accessed like an array.

var set = new Set(["a", "a","e", "b", "c", "b", "b", "b", "d"]);
console.log(set[0]); //Logs undefined

jsfiddle

Sets provide the same methods as a Map.

var set = new Set(["a", "a","e", "b", "c", "b", "b", "b", "d"]);

console.log(set.size); //Logs 5
console.log(set.has("a")); //Logs true
console.log(set.delete("a")); //Logs true, was deleted
console.log(set.has("a")); //Logs false
console.log(set.delete("a")); //Logs false, wasn't deleted, wasn't there.

set.forEach(function (value, key, setObj) {
    console.log(value, key, key === value, set === setObj);
});

set.clear();

console.log(set.size); //Logs 0

set.add("a")
   .add("b")
   .add("c");

console.log(set.has("a")); //Logs true
console.log(set.size); //Logs 3

jsfiddle

Of interest is that delete lets you delete something from a set by value, whereas with an array you would probably do a splice using the value’s index. I like this about Set. Also weird, but interesting is that function for the forEach on a Set gets a value and a key even though there are only values in a set and thus you are given the same value for both arguments.

Sets also have the entries, keys and values methods that a Map has and they give you the same kind of Array Iterator type but in the same vein as the Set’s forEach the entries pair is [value, value], that is you get an array of identical pair values. In addition a Set’s keys and values are identical operations, they both return the same kind of iterator with the same values.

var data, set = new Set(["a", "a","e", "b", "c", "b", "b", "b", "d"]);

for (data of set.entries()) {
    console.log(data); //Logs ["a", "a"]...
}
for (value of set.values()) {
    console.log(value);
}
//Logs the same thing as values() above
for (key of set.keys()) {
    console.log(key);
}

jsfiddle


 WeakSet

WeakSet is similar to WeakMap. You can only store objects, no primitives and you cannot iterate over a WeakSet. The available methods are add, has, delete and clear. There is no size property. Objects are weakly held, if an object held is garbage collected it will not lead to a memory leak. Items held in a WeakSet like a Set are unique, only one of each will be held. The use case for a WeakSet is limited. You could use it to see if you have seen an Object before. For example you could check to see if a user clicked an element before.

As of September 2014, this example does not work in Chrome Canary.

var button, wset = new WeakSet();

for (button of document.querySelectorAll("button")) {
    button.addEventListener("click", function (event) {
        if (wset.has(event.target)) {
            alert("You have clicked this button before!");
        } else {
            alert("First time clicking this button");
        }
        wset.add(event.target);
    });
}

jsfiddle

Perhaps this can be used to prevent double clicking. It wont leak memory so it’s safe to use the HTML element as a key.


I am really excited about Maps and Sets and the new for...of iterator. I can’t wait until they are more widely available. Once they are a lot of ugly anti-patterns necessary to get things done can be thrown in the delete bin. JavaScript is really getting better every day. You should thank a browser developer whenever you meet one for all they have done to make such a great platform. If you notice any mistakes or have questions please don’t hesitate to let me know, I’m @bjorntipling on Twitter.

maps.png

 
863
Kudos
 
863
Kudos

Now read this

Advanced objects in JavaScript

This posts looks beyond everyday usage of JavaScript’s objects. The fundamentals of JavaScripts objects are for the most part about as simple as using JSON notation. However, JavaScript also provides sophisticated tools to create objects... Continue →

Subscribe to Bjorn Tipling

Don’t worry; we hate spam with a passion.
You can unsubscribe with one click.

cC8rOHVQALC8D0IPpb1x