Solving Backbone’s “this.model.view” Problem With Underscore.js
In my last post, I briefly touched on a small problem in the relationship between the view and the model. Here’s the relevant code, again:
This is some awful code, honestly. I didn’t like it when I first wrote it, and I still don’t like it. It coupled my view and my model together in ways that I don’t like. It broke the Law of Demeter. It also confused me – why would the “validated” method of the view execute in the context of the model?! This method is clearly not on the model… but “this” always evaluated as the model, not the view.
Understanding This Problem
I’ve read about “this” in javascript a number of times. It represents the current scope of the code that’s executing, similar to “self” in ruby or “this” in C#. Like ruby and c#, javascript has some interesting rules about what “this” evaluates to depending on when and where you are executing code. In effect, “this” can change at times that you wouldn’t expect based on closures, callback methods, and other code that can modify the scope of the executing code.
Look at the following code for some examples of when you might expect “this” to be something other than what it actually is.
And here’s the html to run this:
Before you continue on to the next images, try to imagine what you’re going to see when you run this code in your browser. Load the page and watch the first two alert boxes go by, then type some text into the input box and tab or click out of it to see the third alert box. Here’s the results, in order:
What did you expect to see in each of the alert boxes? What did you actually see? Personally, I expected all three text boxes to show me “foo”… or maybe the first one should have shown “foo”… but only 1 of the three did – the second call directly to “someView.showIt();” ?! So, what’s going on here? Why is this not behaving the way that you would expect? The answer is in the scope of “this”.
When the first call to someModel.raiseIt(“foo”); is executed, the event “someEvent” is raised. The view has bound to it’s showIt method as an event handler for this event, so the showIt method is called. However, the method is not called by the view. It’s called by the model object. Notice on line 16 of the sample code that the event binding is happening by calling the model’s bind method. We’re effectively telling the model “when ‘someEvent’ fires, call the showIt method from the view class”. This is a closure in action, combined with a function pointer, to make it all work. The end result is that when showIt does execute, “this” is not the view, it’s the model. Since “this” is the model, “this.model” is comes back as undefined, though i would have expected it to come back as the model that is attached to the view.
When the next call to someView.showIt(); is executed, the message box shows us the “foo” data that the previous line had supplied to the model. This really truly baffled me when I first ran my own example code – no joke. I thought I had set something up wrong. As it turns out, it’s correct. Since we are calling someView.showIt(); directly on the view, the scope of “this” evaluates to the view which means “this.model” evaluates to the model correctly. Since we had previously set the model’s “data” to “foo”, evaluating “this.model.get(‘data’)” correctly gives us the value of “foo”.
You may be able to figure out what’s happening with the input box text change, by now. We’re using JQuery to bind to the text box’s change event on 17 of the example code. When you type something into the text box and tab or click out of it, JQuery is firing the showIt method for us. JQuery, as you may have guessed, provides it’s own scope for the method’s execution – the selected element. So, when JQuery fires the event, the scope of “this” is the text box itself, which means “this.model” will be undefined.
But there’s hope!
Underscore.js To The Rescue!
Backbone is built with the Underscore.js library. This little library provides a lot of functionality for Backbone’s models, collections, view, controllers, etc. One of the things that it provides is the “_.bindAll” method, which as it turns out, is our savior to make the showIt method behave consistently.
The Backbone documentation has a brief discussion on Binding “this”. It’s worth reading this section to see what Backbone has to say. The gist of it is that “_.bindAll” will allow you to change the context in which a method is fired, so that the scope of “this” is changed to any object you want. What this means for us, is that we can tell our view to always execute shotIt with “this” scoped to the view! Best of all, it’s a one line change:
Line 2 was added to the initialize method of our view, to bind the view as the context in which showIt is executed. This gives us the view as “this” in the showIt method, and our problems are magically solved! Now when we execute the script again, we get the following results:
These results finally make sense!
The first call sets the model’s data to “foo”, which is then shown to us. The second call doesn’t change the data, it just calls the showIt method directly, which shows us “foo” again. The third call – facilitated by the change event from the input box – doesn’t change the model’s data either, so when showIt it called for a third time, “foo” is shown to us for a third time.
Fixing this.model.view and this.view.loginButton
Now that we understand what’s really going on, we can finally get back to the original login example and fix it.
There’s only a couple of changes we made to the view. We added line 2 to bind the view as the validated method’s scope. We removed the line that assigned “this.mode.view = this;”. And then in the validated method, we changed “this.view.loginButton” to “this.loginButton” because “this” is now the view, which has our login button!
Our sample application still works the way we expect, but we’ve now cleaned up the coupling issue between the view and the model, fixed the Law of Demeter violation, and generally made the code more consistent in behavior by ensuring that the validated method always executes in the scope of the view.
Update: How _.bindAll Works
Matt Shannon sent me a note via twitter, providing me with a few links that describe how the javascript language lets the _.bindAll method work. Of course, this isn’t a breakdown of the bindAll method itself, but they’re the underlying language features that enable it.
- apply: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/apply
- call: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/Call
Thanks, Matt!