Day 7: New Datatypes

When everything is an object other data types might be a strange choice. Why have a Map when you can easily use a JS object as a map? Why have a Set type when you can easily use an Array as a set?

Specific data structures for specific types of data help eliminate bugs. They make data easier to reason about and work with. They give a consistent API for working with that data. In short, they make things a lot better.

Maps and WeakMap

Maps are like objects but you can use anything as a key. For example, you can use a string as a key:

const animals = new Map()
const hedgehogs = [
  {
    name: 'Sonic',
  }
]

animals.set('hedgehogs', hedgehogs)
animals.get('hedgehogs')
Try in REPL

And an object as a key:

const animals = new Map()
const hedgehogs = [
  {
    name: 'Sonic',
  }
]

animals.set(hedgehogs, 'are awesome')
animals.get(hedgehogs)
Try in REPL

Maps are great because they have an explicit size property and avoid any prototype nonsense that can force you to use hacks like object.create(null) or hasOwnProperty when mapping over them.

Maps are also good for storing objects and then looking up them again. We can do things to objects, store meta details about what we did, then look up those details again when we do things to that object again.

But be careful with that for you could run into memory leaks. Which is where WeakMaps come in.

WeakMaps

WeakMaps are like maps but they can only store objects as keys and they delete themselves when there are no references to that object anymore. If you have made an observable or event system you know that you need to clean up after yourself, WeakMap is like a Map that cleans up after itself. Once things stop using the object you have in your WeakMap it will be garbage collected.

For example, a WeakMap is super useful for when you are storing a domElement and when that domElement is done you need to get rid of storing it. It would look like this:

const myMap = new WeakMap()
const cats = document.querySelector('body.cats')
myMap.set(cats, 'cats element')

// Remove the element
cats.parentNode.removeChild(cats)
cats = null

myMap.get(cats)

Sets and WeakSets

Ever had an array of cats but wanted it to be unique? Because that orange tabby kept showing up no matter how much you tried? Sets are basically arrays, with all the same semantics except Sets only let in unique things. They work like this:

const mySet = new Set()
const cat = {
  name: 'Mr Ferguson',
}
mySet.add(cat)
mySet.add(1)
mySet.add('a string')
mySet
Try in REPL

You can add items to a set using add:

const mySet = new Set()
const cat = {
  name: 'Mr Ferguson',
}
mySet.add(cat)
mySet
Try in REPL

And delete items using delete:

const mySet = new Set()
const cat = {
  name: 'Mr Ferguson',
}
mySet.add(cat)
mySet.delete(cat)
mySet
Try in REPL

And check for items using has:

const mySet = new Set()
const cat = {
  name: 'Mr Ferguson',
}
mySet.add(cat)
mySet.has(cat)
Try in REPL

To retrieve an item you have to loop over all the items. You can iterate over items using a for loop:

const mySet = new Set()
mySet.add(1)
mySet.add(2)
mySet.add(3)
mySet.add(4)
for (let item of mySet) console.log(item)
Try in REPL

WeakSet

WeakSets are exactly like sets except their items are deleted automatically when their are no references to that object anymore. They are very useful for storing anything that might get removed and no longer be needed. You don't have to bother keeping track of when all the references are gone.

A good use case is dom nodes:

const myMap = new WeakSet()
const cats = document.querySelector('body.cats')
myMap.add(cats)

// Remove the element
cats.parentNode.removeChild(cats)
cats = null

myMap.has(cats) // => false

Proxies

I have no idea what I am doing.

Ever built a validator and had to remember to pass things through it? Then in some place you forgot to do it? Or maybe the boilerplate was really annoying? One day you accidentally spit out properties you didn't mean to on your JSON API and you are like "that is it. I'm switching to a library". Manual validators are harder than they seem.

ES6 brings native support for the Proxy pattern to JavaScript. Proxies let you put functionality in front of a target object and intercept things that are done to it. They are extremely useful for things like validation and observers.

They look like this:

class Cat {
  get powers() {
    return this.catPowers
  }

  set powers(powers) {
    this.catPowers = powers
  }
}

const handler = {
  get(target, prop) {
    console.log(`getting ${prop} in ${target}`)
    return target[prop]
  }
}
const cat = new Proxy(new Cat(), handler)
cat.powers = ['knocking things off tables']
cat.powers.toString()
Try in REPL

Let's look at some ways we can use proxies

Validators

Proxies are perfectly suited for constructing validators. You can intercept setters and getters then throw errors when the values fail to pass rules. A simple validator for a cats age would look like this:

const validator = {
  set(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('Age should be a number')
      }
      if (value > 30) {
        throw new RangeError('Cats are tragically not immortal')
      }
    }

    obj[prop] = value
    return true
  }
}

const cat = new Proxy({}, validator)

cat.age = 10
console.log(cat.age)
person.age = 200 // => Throws error
person.age = 'young' // => Throws error
Try in REPL

Observers

Proxies make observers really dead simple. We simply need a handler that we can pass a callback to and then every time the proxy is called we will call the callback with the value. Let's wrap the handler in a function first:

const createHandler = (callback) => {
  return {
    set(obj, prop, value) {
      callback(prop, value)

      obj[prop] = value
    }
  }
}

Now we need a proxy constructor that takes an object and a handler and crafts a new Proxy with it:

const createProxy = (obj, callback) => {
  return new Proxy(obj, createHandler(callback))
}

Then we can string it all together like this:

const callback = (prop, value) => {
  console.log(`prop: "${prop}" value: "${value}"`)
}

const createHandler = (callback) => {
  return {
    set(obj, prop, value) {
      callback(prop, value)

      obj[prop] = value
      return true
    }
  }
}

const createProxy = (obj, callback) => {
  return new Proxy(obj, createHandler(callback))
}

class Cat {
  constructor(name) {
    this.name = name
  }
}

const cat = createProxy(new Cat('Ferguson'), callback)
cat.name = 'Awesome Cat'
Try in REPL

That is it! You have learned ES7!