JavaScript's Slippery this Reference and Other Monsters in The Closet
One problem in WWWTC # 16 is the assumptions Estelle makes about this. In C# you
don’t need to explicitly use the this keyword to reference an instance field from
inside an instance method. It’s tempting to write a method like Estelle’s …
InputManager.prototype.resetToInitialValue = function()
{
_input.value = _initialValue;
}
… only to find out the method tries to access a global variable named _input and
a global variable named _initialValue. In ECMAScript we need to explicitly bring
this into play to reference entries in the object’s own dictionary:
InputManager.prototype.resetToInitialValue = function()
{
with(this)
{
_input.value = _initialValue;
}
// OR
this._input.value = this._initialValue;
}
Thanks to delegate inference in C#, it’s also tempting to think we can assign an
instance method to a DOM event …
function InputManager(input)
{
this._input
= input;
this._initialValue
= input.value;
// won't work
this._input.ondblclick = this.resetToInitialValue;
}
… only to find out we’ve simply given the DOM event a reference to a function object.
The DOM element will happily invoke the function directly and without using an instance
of an InputManager object. Our this reference inside of the resetToInitalValue function
is then useless. Similar code in C# will work, because the C# compiler
will magically create a delegate behind the scenes, and that delegate will manage
an object reference, and invoke an instance method through that object reference.
There are mechanisms in ASP.NET AJAX to fix the above scenario, but in the end it
boils down to giving the DOM element a function object that can remember the proper
this reference. A simple approach would look like:
function createDelegate(object,
method)
{
return
function() { method.apply(object,
arguments); }
}
function InputManager(input)
{
this._input
= input;
this._initialValue
= input.value;
this._input.ondblclick
= createDelegate(this,
this.resetToInitialValue);
}
Here we are using the magic of a JavaScript closure to create and return a new function
object from inside createDelegate. The DOM event will reference this new function
object. When this new function object is invoked by the DOM element, the new function
sets up a proper invocation of resetToInitialValue by using Function.apply and passing
through the InputManager reference to use as the this reference.
We’ll explain this topic of closures and Function.apply more thoroughly in the future.
Those fixes should get the code working for now, but what else in that code was
a monster in the closet?
For one, the event handler registration could use some work. There are tons of JavaScript
code and articles written on the topic of how to do safe, non-desctuctive, cross-browser
event registrations, so for now I’ll just say if you are using something like ASP.NET
AJAX’s $addHandler technique your code will improve over the above approach.
Secondly, it’s good practice to clean up event registrations. The code sets up a
scenario where each InputManager holds a reference to DOM element (in the _input
field), and that same DOM input holds a reference back to it’s InputManager (via
the function object / event handler that formed a closure over the InputManager
instance). In the past, these circular references created memory leaks and were
a common “memory leak pattern” in Internet Explorer. I say “in the past” because
these problems are fixed in IE7 (and in IE6 (with an update)). Still, the generally
accepted practice in today’s AJAX patterns is to clean up afterwards, which feels
like the right thing to do as not all browsers are perfect in figuring out these
circular references.