How YUI’s event listeners changed the way I write JavaScript
I really like Prototype.js. Its inclusion in Ruby on Rails got me writing JavaScript again. We at Schematic don’t recommend Prototype for client projects however, and I’m glad because the library or framework you use can greatly change the style in which you write code. And sometimes that can make you a better programmer.
Working with YUI helped trigger an epiphany for me this past week. Credit for laying the foundation for this moment goes to the sample chapter on Inheritance of Douglas Crawford’s new book, JavaScript: The Good Parts. I did not understand the practical usage for the techniques in the chapter at first because I learned Object Oriented design in classical OO languages like Actionscript 2, PHP5, and Ruby (which Prototype imitates as best as possible).
I’ve tried to maintain this style in JavaScript, especially in large projects like Silverlight 1.0 RIAs. I usually structured classes like this:
-
var MyClass = function() {
-
// MyClass constructor
-
}
-
MyClass.prototype ={
-
method : function() {
-
// do stuff with the "this" object
-
}
-
}
-
-
var myInstance = new MyClass();
I eventually extracted the new Class.create function from Prototype.js 1.6.0, because it gives you an elegant syntax for inheritance, mixins, and access to overridden methods without much overhead. It basically does the same as the above with some extra magic for creating the inheritance chain.
There’s nothing inherently wrong with this approach, and I think that anyone who tells you otherwise is a bit of a zealot. But it’s reliance on the this object creates problems, especially with event listeners. YUI made this very apparent:
-
var GUIElement = function() {
-
var button = document.getElementById('button');
-
YAHOO.util.Event.addListener(button, 'click', this.onButtonClick);
-
}
-
GUIElement.prototype = {
-
onButtonClick : function(event) {
-
// this === button
-
}
-
}
-
-
var element = new GUIElement();
The scope of the event handler is the button, which I believe follows the standard. However, I usually want this to refer to the instance of the class so I can access other methods and properties. The bind method in Prototype makes this easy by creating an anonymous function that executes the function in the specified scope:
-
button.observe('click', this.onButtonClick.bind(this));
There are a few problems with this approach.
bindusesFunction.applywhich adds overhead. It’s usually negligible but with JavaScript optimization is always a top priority.- We’re not using Prototype! In this case, the client is a large e-commerce site and we’re only coding a small section of it. Prototype doesn’t necessarily place nice with other libraries since it defines a lot of global variables and augments the prototypes of build-in objects (like adding
bindtoFunction).
You can specify the scope of the event handler with YUI, but it has the same effects as the first point above.
-
// a "truthy" last argument makes the second-to-last argument the function scope
-
YAHOO.util.Event.addListener(button, 'click', this.onButtonClick, this, true);
This felt very awkward to me, like I was doing something I wasn’t supposed to do. It’s perfectly valid but the awkwardness got a few gears turning in my head. I don’t think I can adequately explain my train of thought without turning this into an unreadable ramble, but here is how I’ve started writing components (using the module pattern):
-
var GUIElement = function() {
-
-
// define constants, like shortcuts to the YUI library
-
var Event = YAHOO.util.Event;
-
-
// this "constructor" creates an object and passes it to functions
-
// instead of calling methods on the object itself
-
function create() {
-
var object = {}; // the instance
-
object.button = document.getElementById('button');
-
addEvents(object); // pass it to the function
-
return object;
-
}
-
-
// we'll call the object "that" so it kind of looks like "this"
-
function addEvents(that) {
-
// pass "that" to the event handler, but don't change scope
-
Event.addListener(that.button, 'click', onButtonClick, that);
-
}
-
-
function onButtonClick(event, that) {
-
// do stuff with that
-
// as a bonus, "this" still refers to the button
-
}
-
-
return {
-
create: create // make create a public function
-
}
-
}();
-
-
// the module pattern means no more "new" keyword
-
var element = GUIElement.create();
And all of a sudden, Crockford’s techniques make sense! My paradigm shift is really just two ideas:
- Encapsulate code into factories, not classes.
- Functions act upon objects instead of being methods of the object that act upon itself (
addEvents(that)instead ofthis.addEvents()).
I’ve yet to fully explore the ramifications of these techniques, but I wanted to try writing this down to see if it still makes sense. I won’t throw Class.create out of my toolbox just yet, but if I can understand an elegant way to do inheritance–or more likely, composition–I do think this will be the way to go 90% of the time. More on this topic to come.
Addendum: Curiously, Luke W posted an article while I wrote this about the exact component I was refactoring when this epiphany hit me.
[...] Moore + Douglas Crockford = Epiphany: Lenny Burdette writes on his Rule52 blog about "How YUI’s event listeners changed the way I write JavaScript", bringing together insights from the sample chapter of Douglas Crockford’s JavaScript: [...]
In the Wild for June 20 » Yahoo! User Interface Blog — June 20th, 2008 at 5:04 pm
This pattern actually opens up more problems for you than if you were to use the prototypal method with partial function application that .bind() or YUI’s scope correction offers.
The reason is because prototypal inheritance is incredibly memory efficient, whereas the Module pattern is not.
Here’s why:
When you create an object with new, for every object that you create, only one copy of the methods on that prototype is stored in memory.
Your current implentation seems to be more of a factory, and on the face of it, it’s not super memory intensive. But there are going to be some major issues as soon as you start adding some public methods on the object that is returned from .create().
Because your .create factory is returning a new object every time, but this object won’t be taking advantage of the prototypal inheritance, and so you will have multiple copies of the object floating around in memory.
This would be much less efficient than the scope correction of either Prototype or YUI.
I believe Class.create takes advantage of the prototype, and there are others that do as well.
Just some thoughts
Nate Cavanaugh — March 4th, 2009 at 3:03 pm