My personal love for Prototype.js is evident, and that love has grown further as I have become familiar with object oriented programming using the framework. However, my appreciation has come at the expense of a few growing pains, one of which is maintaining the this
keyword when dealing with event handlers. Normally in JavaScript, when you add an event handler such as onclick
to an element, the triggered function can use the this
keyword to refer to the element. Likewise, a class uses the this
keyword to reference its member variables and methods. So, what happens when you attach an event to an element from within a class? Which object receives the attention of this this
keyword?
Let’s take a look at a simple class that adds an onclick event to a link. A simple line passing in the link will initialize the class:
new foo($('mylink'));
And then the class itself. Notice how we are adding the onclick event to the link using Event.observe (similar to addEvent()).
var foo = Class.create();foo.prototype = { initialize: function(ctrl) {
this.ctrl = ctrl;
this.ctrl.className = "testing";
Event.observe(this.ctrl, 'click', this.dosomething, false);
}, dosomething: function() {
alert(this.className);
// results in alerting 'testing'
}}
Once the link is clicked, the method dosomething()
will fire. When we use the this
keyword we are getting a reference to the link, not the class. But what if we want to access the class. For example, we may want to call a member function as shown below.
dosomething: function() {
this.dosomethingelse();
},dosomethingelse: function() {
// do something else
}
The code above will produce the wonderful error:
> this.dosomethingelse is not a function
And the reason we get that error is because JavaScript is looking for a method of the link called dosomethingelse()
. Instead, we want JavaScript to look for a method of the class called dosomethingelse()
. In order to make the code above work, we need to preserve the this
keyword. To do that, change this line:
Event.observe(this.ctrl, 'click', this.dosomething, false);
to this:
Event.observe(this.ctrl, 'click', this.dosomething.bindAsEventListener(this), false);
The difference between the two lines is that the second one has bindAsEventListener(this)
added to it. bindAsEventListener()
takes one parameter: the object that the this
keyword should reference. If you wanted the this
keyword to reference the link instead of the class, you could put this.ctrl
as the parameter:
Event.observe(this.ctrl, 'click', this.dosomething.bindAsEventListener(this.ctrl), false);
So that is how the this
keyword works within the Prototype.js class structure. Now, if anyone can help elaborate on the functionality of bindAsEventListener()
, that would be great. This post show that it works, but I could use some clarification on why it works.
From the Microsoft script documentation (I use it often):
“The call method is used to call a method on behalf of another object. The call method allows you to change the object context of a function from the original context to the new object”
To clarify, the bindAsEventListener essentially allows you to change the context of “this” for the function you are binding to. This is done via the call method which bindAsEventListener uses. bindAsEventListener also passes the event object as an argument so that it’ll be available within the function called.
I highly recommend downloading the Script documentation from Microsoft at http://microsoft.com/scripting/ (they’ve changed their system and requires a genuine OS to download)
You should read PPK’s blog on finding a solution,
http://www.quirksmode.org/blog/archives/2005/10/_and_the_winner_1.html
And the final entry which solves
http://ejohn.org/projects/flexible-javascript-events/
I’m not sure where I heard it, but the bind method was recently shown to me as well because I too was frustrated in Prototype’s lack of ‘this’ preservation for IE. It’s cool to see that it can be done, but it looks too much like a hack to me even though it’s perfectly valid.
That’s one reason I still just stick to my own addEvent function which is an alteration of the addEvent() contest winner along with some other memory leak prevention tweaks.
I was memory leaks was mentioned above.. they can be a big problem (in IE, real browsers do fine) but fortunaly if you add an event handler using Event.observe prototype will automaticly clean up on unload, to help IE with the garbage handling..
Well I haven’t looked at the bindasEventlistner function but I did take a look at the code to bind and what it does is pretty simple.
Basically instead of binding the function you pass to the event it binds a new anonymous function to the event. This anonymous function has one line which uses the call (or is it apply?) method to call the method you passed with the this variable set to the object you passed. Pretty simple so far.
The only place this gets complicated (conceptuall the code is trivial) is in creating that anonymous function. Once you understand how functions work it is pretty obvious but it can be confusing at first. Essentially the anonymous function defined by bind is a closure, meaning it retains the scope it had while created, i.e., it can still access the object you passed into bind.
If you want some help understanding closures in JS I found this great page on closures in JS
Peter
Oops, I screwed up in my post above. Ignore the entire part about binding to an event I had a brain fart because we were talking about events.
All bind does is return a function which is bound to a specific object, i.e., will have the same this pointer no matter where you call it. This is usefull if, for instance, you want to pass a method around.
So where I wrote “instead of binding the function you pass to the event” ignore that bit. What I should have said was that bind returns a new anonymous function which calls your function with the appropriate this pointer.
Use an indirect method call to avoid conflicting this
Hello, I often use what I call an indirect method call to solve the conflicting this. See code below:
var foo = Class.create(); foo.prototype = {
};
I just use a
var self = this;
within the class’s (global) declarations. This makes theself
object point to the class throughout the class. Later, when I want to use the classes methods in an anonymous function or a closure within the class, I simply useself.doSomething();
That way, I get to use the real power of the
this
keyword with its changing values depending on the context, while still retaining the reference to the class whenever I need it.
This means of “enforcing” scope is used in dragdrop.js, though the one you describe is more compact. They have things like this:
this.eventMouseDown=this.startDrag.bindAsEventListener(this);
followed by:
Event.observe(this.handle, “mousedown”, this.eventMouseDown);
this is one case where putting it all smashed together (as you did) improves readability…
ahem. let me try that again:
This means of “enforcing” scope is used in dragdrop.js, though the one you describe is more compact. They have things like this:
this.eventMouseDown=this.startDrag.bindAsEventListener(this);
followed by:
Event.observe(this.handle, “mousedown”, this.eventMouseDown);
this is one case where putting it all smashed together (as you did) improves readability…
Thanks Peter for one of the best and most useful javascript related reference ever!
I’m trying to decide if Prototype offers any value to me. Can anyone tell me why all these observe and bind syntaxes are any better than the “var self = this”??
The functionality of bindAsEventListener() is explained in detail on the page of its creator Daniel Brockman http://www.brockman.se/writing/method-references.html.utf8
“I highly recommend downloading the Script documentation from Microsoft”
I had a look at it, and now I’m confused. Am I the only one wondering what windows OS scripting has to do with javascript?
Everyone needs a hug.
Everyone needs a hug.
Yes, everyone needs a hug!
I love you more than life itself. You just solved the problem I’ve been struggling with for the past several hours. Thanks!
Thank you very much for ferreting this out. I wasted an entire day tinkering ….