Tuesday, 1 April 2008

You must remember 'this'

Of all the tech blogs in all the sites in all the worldwide web, you walk into mine...

If you hang out in JavaScript-oriented newsgroups like these for any length of time, you will eventually see some variation of this question:
Hey, why doesn't this work?
function MyWidget(name)
{
this.name = name;
this.element = null;
}
MyWidget.prototype.showName = function()
{
alert('The name is ' + this.name);
}
MyWidget.prototype.hookElement = function(element)
{
this.element = element;
Event.observe(this.element, 'click', this.showName);
}
function test()
{
var widget;

widget = new MyWidget('Test Name');
widget.hookElement(document.getElementById('testDiv'));
}
"testDiv" is a div in the document, and I know that I'm not calling the test() function before the DOM is loaded, so why is it when I click the div I get the message "The name is undefined"?!
(As always, I'm using some convenience syntax in the above for hooking up the event handler.)

The OP (original poster) might even follow on with:
I even tried changing the observe() line to this:
Event.observe(this.element, 'click', function() {
this.showName();
});
because I heard somewhere that you have to do that, but that's even worse, it causes an error saying this.showName() isn't a function?!
The issue here is that the OP hasn't quite grokked "this" and its special role in the JavaScript world.

I talked a bit about 'this' over here, but I wanted to do a post focussing on the specific pitfall the OP above, like so many of us, has fallen into (forgetting 'this') and how you deal with it.

Let's look at what's wrong with this line first:
Event.observe(this.element, 'click', this.showName); // Wrong
JavaScript doesn't have methods (see link above), and so this.showName just returns a function reference with absolutely no connection to the instance the OP wanted to bind to the element. It's just a function. (Used properly, this is a powerful feature, but in this situation it's causing the OP some trouble.) Recall that showName is defined like this:
MyWidget.prototype.showName = function()
{
alert('The name is ' + this.name);
}
Within the code, 'this' is determined not by where the event handler is set up, but by how the function gets called. Most likely, 'this' will be a reference to the element that was clicked ('testDiv'), because modern browsers use the element related to the event as 'this' within event handlers. Consequently, this.name is undefined unless the element in question happens to have a name attribute.

So to get the intended effect, you have to wrap the call to this.showName() so that 'this' is the MyWidget instance when the code gets executed -- you must remember 'this'. Which is probably what the OP heard about when he tried this:
Event.observe(this.element, 'click', function() {
this.showName();
}); // Still wrong
This is getting closer, and in fact it would work if we were using a variable to reference the widget rather than 'this', but because it's 'this', we actually still have exactly the same problem we had before: When the event handler gets called, 'this' is the element, not the widget, and so there's no showName() function to call.

So how do we deal with this? Well, here's one approach I've seen to rewriting the hookElement function:
MyWidget.prototype.hookElement = function(element)
{
var self;

this.element = element;
self = this;
Event.observe(this.element, 'click', function() {
self.showName();
});
}
This works because we're no longer using 'this' within the event handler, we're using 'self' (the event handler has access to 'self' because it's a closure; more here). So that solution works. I can't say I like it, though. It just feels...hacky, I guess. But still, it works, and although it looks a bit funny the first time, if you're familiar with the idiom you read right past it thereafter. You just need to be sure the closure isn't unnecessarily preserving some other big amount of data from elsewhere in the function.

Personally, though, I prefer using a reusable "binding" function. Many JavaScript toolkits have these (such as Prototype's bind() and bindAsEventListener()), but it's not complicated:
function bind(f, obj)
{
return function() {
return f.apply(obj, arguments);
};
}
This is a function factory: It creates functions that, when called, will call the given function with the given object set as 'this' (using JavaScript's convenient apply() function; insert your own "The fundamental things apply" joke here). Now we can rewrite the OP's hookElement function like so (changes from the original at the top in bold):
MyWidget.prototype.hookElement = function(element)
{
this.element = element;
Event.observe(this.element, 'click',
bind(this.showName, this)
);
}
You might be wondering why we have to specify 'this' twice. Remember that this.showName just returns a function reference, with nothing about the instance (we could replace this.showName in the above with MyWidget.prototype.showName if we liked). If we want bind() to know what instance we want to bind the function to, we have to specify it -- the this at the end.

And that's it! Now the event handler works as the OP expected it to.

3 comments: