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?(As always, I'm using some convenience syntax in the above for hooking up the event handler.)"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"?!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'));
}
The OP (original poster) might even follow on with:
I even tried changing the observe() line to this:The issue here is that the OP hasn't quite grokked "this" and its special role in the JavaScript world.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?!Event.observe(this.element, 'click', function() {
this.showName();
});
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.
Further Reading: Christophe Porteneuve on binding
ReplyDeleteAnother great article. So happy I saved it : )
ReplyDeleteThanks for the article, great place to stumble onto
ReplyDelete