jQuery Event Bindings On Javascript Objects With A Length Property
The other day, I ran into a very interesting behavior involving jQuery event binding and Javascript objects. While it is still an open question as to whether or not jQuery truly supports non-DOM-object event binding, it is a feature that appears to be actively included in the jQuery library (even by John Resig himself). That said, I found myself stuck for a good 20 minutes the other day trying to figure out why an event binding on a given Javascript object was not working.
After a good amount of debugging, I finally narrowed the problem down to the Length property. One of the Javascript objects to which I was binding events had a length property with a default value of zero. It appears that if the Javascript object has a length property of zero, triggered events on the object don't actually fire. It's as if jQuery is using the length property of the composed object rather than that of the parent jQuery collection. To demonstrate this, take a look at the code below:
<!DOCTYPE HTML>
<html>
<head>
<title>Javascript Object Binding Caveat</title>
<script type="text/javascript" src="jquery-1.4.2.js"></script>
<script type="text/javascript">
// Create a basic Javascript object. Notice that this one
// does not have a length property.
var foo = {
size: 0
};
// Create another basic Javscript object. This time, we are
// going to create a LENGTH property.
var bar = {
length: 0
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// Now that we have our Javascript objects, we are going to
// bind an events to them. First, we have to wrap them in
// a jQuery object for the bind/triggering.
var eventHandler = function( event, parent ){
console.log( parent + " : Event triggered!" );
};
// Bind both event handlers.
$( foo ).bind( "custom", eventHandler );
$( bar ).bind( "custom", eventHandler );
// Now that we have bound our event handler, let's trigger
// the custom event on each object.
$( foo ).trigger( "custom", "foo" );
$( bar ).trigger( "custom", "bar" );
</script>
</head>
<body>
<!--- Intentially left blank. --->
</body>
</html>
As you can see, I have created two Javascript objects - one with a size property and one with a length property (each defaulted to zero). Then, I bound a "custom" event to each object and tried to trigger it. When I do this, I get the following console output:
foo : Event triggered!
As you can see, the event on the "bar" object never fired.
To dig into the problem a bit deeper, I decided to log each object to the console after the page had loaded. I thought this might give me some more insight as to where things were going wrong. Here is what I get for the Foo object:
jQuery1272285849095: 1
size: 0
You can see in this console output that jQuery has added the "expando" property to the Foo object. This is the property that it uses to maintain all of the data associated with the object. Part of that data is the "event" key which contains the event handlers bound to this object.
When I do the same with the Bar object, here is the output that I get:
length: 0
As you can see here, jQuery did not add the "expando" property to the bar object. From this, we can conclude that the problem was not with the event triggering but rather with the event binding. After all, we cannot trigger an event that was never bound to the target object.
As a further experiment, I tried to default the length property of the bar object to a non-zero value. When I do this, I actually get the following Javascript error:
jquery-1.4.2.js (line 1560)
elem is undefined
if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
This is in the jQuery.event.add() method. While I am not 100% sure how all the event binding works, I am pretty sure that this exception is being raised in the event binding, not the event triggering phase of the configuration. Looking at the underlying jQuery code, it looks like this is where jQuery is attempting to use the length property of the underlying Javascript object.
Being able to leverage jQuery's event binding / handling mechanism on Javascript objects is something that is very powerful. It appears, however, that there are some caveats that cause this mechanism to fail critically. I, for one, would love to see this feature supported on Javascript objects in its completeness. Of course, doing that might take too much rewiring of the existing event handling code.
Want to use code from this post? Check out the license.
Reader Comments
I decided to dig into this myself and I think I've narrowed it down to the jQuery.merge function which is used when you pass ANYTHING with a non-null (not undefined or null) length property as the first argument to $().
jQuery.merge will assume that it's an array-like object and will continue to go through it and "merge" its array items with the current jQuery instance. Of course, your {length:0} object has no array items -- jQuery thinks it's just an empty array, and so what you get is an empty jQuery instance.
To get around this issue, you could pass the object within its own array to make it absolutely clear that you want your object to be a part of the jQuery instance:
So instead of:
$({length:0});
Do:
$([{length:0}]);
I think it must be how jQuery uses the native length property for sanity checks internally.
For fun if you were to set bar's length property to 20... when you trigger your custom event, it should run it 20 times?
@James,
Yeah, good idea. I tried that as a step to debug and noticed that it worked, but then forgot about it. What you're saying about reason it works makes total sense.
Thanks for digging into that deeper!
@Garrett,
Unfortunately, if you set the length to a non-zero value, it errors out trying to iterate over a non-populated array.