Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
put-a-closure-on-it

Put a closure on it

Sometimes you'll have objects that manage state along with event handling. This happens frequently in MVC apps. Let's start with a messy example:

var Launcher = function(rocket) {
  this.rocket = rocket
}

Launcher.prototype.isReady = function() {
  return rocket.isFueled() && rocket.isManned()
}

Launcher.prototype.launch = function(e) {
  if(this.isReady()) {
    $.ajax('/launch', { rocket_id: this.rocket.id, type: this.type })
  } else {
    console.log("can't launch yet")
    e.preventDefault()
  }
}

var pad = new Launcher(rocket)

$('#launch').click($.proxy(pad.launch, pad))

The anti-pattern above, in which an event is bound to an instance function on an object is common in many MVC frameworks and client-side code. The launch function here has to be bound to the Launcher instance in order for the call to this.isReady() to work within that function. this must be bound to the Launcher instance rather than the event. jQuery offers the $.proxy function to accomplish this, while CoffeeScript provides a built-in workaround with the => operator. This example also illustrates the problem where an object begins to take on too many responsibilities - state and event management.

A less messy solution is to create a prototype-specific handler object that wraps each event in a closure:

var Launcher = function(rocket) {
  this.rocket = rocket
  this.type = 'heavy'
}

Launcher.prototype.isReady = function() {
  return rocket.isFueled() && rocket.isManned()
}

Launcher.prototype.launch = function() {
  if(this.isReady()) {
    $.ajax('/launch', { rocket_id: this.rocket.id, type: this.type })
    return true
  } else {
    return false
  }
}

Launcher.handlers = {
  launchClick: function(launcher) {
    return function(e) {
      if(!launcher.launch()) {
        e.preventDefault()
        console.log("can't launch yet")
      }
    }
  }
}

var pad = new Launcher(rocket)

$('#launch').click(Launcher.handlers.launchClick(pad))

This drives more of the DOM-related logic into another object and helps code clarity by specifically enumerating which objects interact with a given event. You also retain access to the this that points to the button being clicked.

A more detailed discussion of this particular closure pattern is here. Additionally, see Hootroot.com's source for a real-world example.

chuyik commented Feb 4, 2015

I think the handlers style is not so consistent with the prototype style, maybe we could use Function's built-in method, bind.

 $('#launch').click(pad.launch.bind(pad))

ES6 solved the problem with arrow functions:

$('#launch').click(e => pad.launch(e))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment