Maps and Sets

One of the super useful new features of ES6 is Map, Set and its friends WeakMap and WeakSet. We've always had objects in javascript, but the reason why this is a big deal is that maps map based on an object reference, not just a string.

Here's a few ways its amazeballs useful.

Handling lifecycle events

Lets say I have a bunch of objects I made. Sometimes I want to delete them:

var allThings = {}; // For iteration.  
// Using an object instead of a list because removal is O(1) instead of O(N)

var nextId = 1;  
function makeThing(x, y) {  
  var thing = {x:x, y:y, dancing:true};
  allThings[nextId++] = thing;
}

function deleteThing(thing) {  
  // Uh crap, I can't delete it without its ID!
}

Oh damn, alright maybe I can write deleteThing like this:

function deleteThing(id) {  
  delete allThings[id]; // woo we did it
}

… Except now I need to change all my code to pass around ids instead of simply object references. This is really annoying. And sometimes this even bleeds into your data structures and you end up storing object IDs instead of simple references. Ugh.

You can fix this by putting the ID into the thing itself, but its weirdly redundant and circular.

With ES6…

Now with ES6 we don't have this problem anymore.

var allThings = new Set; // So fancy!

function makeThing(x, y) {  
  var thing = {x:x, y:y, dancing:true};
  allThings.add(thing);
}

function deleteThing(thing) {  
  allThings.delete(thing);
}

Tada! I don't need object IDs at all now. I can just have a reference to the object and do everything with that.

Adding more properties to objects

Another way this stuff is redonk useful is adding properties to objects you shouldn't edit, like DOM elements:

var elem = document.getElementById("todo-list");  
elem.model = {…};  

This is really dangerous:

  • Who knows what arbitrary properties will be added to DOM nodes in the future.
  • Its easy to have memory leaks from stuff like this

WeakMap is a map where if the key is GCed, any associated objects in the map will also be collected automatically. Its sort of another way to set obj.foo = bar without actually adding a .foo property to the object.

It looks like this:

var elem = document.getElementById("todo-list");  
var models = new WeakMap;  
models.set(elem, {…});

// Then models.get(elem) to get it back

Speaking of model data, its common to need binding data for your model. Historically the only way to do that was to wrap the data itself in a fancy class which also included the bindings:

function Model() {  
  this.data = {x:5, y:6}; // The actual data.
  this.bindings = {…}; // Important junk
}

Model.prototype.set = function(k, v) {  
  // Gross wrapper is gross
  this.data[k] = v;
  this.bindings.update();
}

myModel.set('x', 10);  

With weakmaps and you can dump the model class entirely:

var data = {x:5, y:6}; // Just the good stuff.

// The bindings themselves are separate.
var bindingForData = new WeakMap;  
bindingForData.set(data, {…});

function refresh(data) {  
  var bindings = bindingForData.get(data);
  bindings.update();
}

data.x = 10; // Much better.  
refresh(data); // Hmm… still a little gross.  

With Object.observe in ES7 we can finish the job and get rid of the explicit call to update bindings:

Object.observe(data, function(changes) {  
  for (var c of changes) { // ES6 iterator
    var bindings = bindingsForData.get(c.object);
    bindings.update();
  }
});

data.x = 10; // BOOM!  

I've started experimenting with this sort of thing for JSON-OT. With both Object.observe and Array.observe, I think its possible to generate OT operations from object mutation directly without needing any helper setters at all.

Downsides

There's a few annoying things. First, weakmaps are only available in IE11+. So web frameworks like derby probably can't use it for awhile. Object.observe is literally only available in Chrome, Node and io.js.

Another thing I found annoying is that I often wanted a map from a pair of elements:

var m = new Map;  
m.set([a, b], c);  
// But…
m.get([a, b]); // undefined!  

It makes sense when you think about whats going on. The map is mapping based on a reference to the array. If you need to fetch the objects, you need a copy of that array. And for that you need a map from (a, b)! Argh. Its a locked-keys-in-car moment.

You can make it work with a map-of-maps.

You have the same problem if you want a set of pairs:

var s = new Set;  
s.add([a, b]);  
s.has([a, b]) // false!  

... Which is solvable with a map of sets.

This came up enough in boilerplate-jit that I made Map2 class and Set2 classes (And Map3, Set3 and SetOfUnorderedPairs…). They're all hidden in a utility file with no tests but I keep wanting them, so I broke Map2 and Set2 out into their own nodejs modules complete with tests and ES6 iterator support. They're in npm:

var Map2 = require('map2');  
var m = new Map2;  
m.set(a, b, c);  
m.get(a, b) // c.  

Ping me if you want the same treatment to Map3 and Set3.

I hope that was useful and interesting. Some of the new ES6 features are radically changing how we'll write our javascript & web apps. Its well worth keeping abreast of the changes just for personal sanity - this stuff makes our lives measurably easier.