Bend Over, jQuery… This Won’t Hurt a Bit…

I have my first large-scale public presentation coming up in about a month and a half, at the jQuery Conference here in Austin, TX. My topic is about getting the most out of jQuery UI Widgets, mostly dealing with some tips and tricks I’ve either learned or developed over my own time in working with the factory. Some things I’d planned to present were still just conceptual at the time I made my proposal. One of them almost bit me in the ass because of something I hadn’t considered, but having just nailed it with some help from something buried in the bowels of jQuery, I feel compelled to write an article about it, to deviate from some of the more basic, foundational things I’ve been writing about lately.
 

jQuery.expando and jQuery.cache

 
jQuery.expando is a randomly computed alphanumeric value that’s generated for each copy of jQuery that’s loaded in a page (you can run multiple versions of jQuery in a page using the noConflict() function, though I don’t personally advise it). This expando property is used to store a unique id on any given element, the value of which can be used as a key in the jQuery.cache object to store data relevant to that element. Even if you’re not familiar with jQuery.cache, you’ve more than likely made use of it before. If you’ve ever bound an event listener with jQuery or stored data on an element via jQuery’s data() function, you’ve made use of jQuery.cache. What is it and what does it look like? Here’s a sample:

{
    1: {
        events: {
            click: [],
            mouseup: [],
            keydown: []
        }
    }
}

In the above example, the expando property stored a value of “1” on the element in question, which is what was used to key our jQuery.cache item. The events property stores an object map of any events pertaining to this element, keyed by event type (click, mouseup, keydown, etc). Whenever one of these events is triggered on the element in question, it looks up data in those stored event objects to see what function handler to trigger as a callback. Additionally, every time it stores a reference to a function, it associates a unique guid with it to use for comparison when unbinding a callback (it ports a guid forward to a proxy()-wrapped version of a function, as well, and considers them identical for the purposes of unbinding callbacks).
 

Javascript Function Pointers

 
Why is this important? These event objects store properties that reference our callbacks long after the code has been executed that binds up those listeners. Javascript stores function expressions (named variable or object members that store functions) as pointers to the memory location where the function is stored, rather than as a reference to the memory location itself. It has no native method of directly referencing a memory location for function expressions. If you’re familiar with PHP, you can use an ampersand prefix on a variable name to reference its memory location directly, and edit the contents that are stored there. That means you can do things like this:

$foo = "blah";
$bar = $foo;
$bar = "blahblah";
echo $foo; // Outputs: blah

$bar = &$foo;
$bar = "blahblah";
echo $foo; // Outputs: blahblah

Javascript has no facility to offer the same kind of variable manipulation. If you tried something similar with a function expression in javascript, you would wind up with the following results:

var foo = function () {},
    bar = null;

bar = foo;
bar = function () { alert("Hello world!"); }
foo(); // Empty function
bar(); // Alerts: Hello world!

bar = foo;
foo = function () { alert("Hello world!"); }
foo(); // Alerts: Hello world!
bar(); // Empty function

As you can see, modifying either variable has no effect on the other variable. The variables remain pointed at the memory location for the given function to which they’re assigned. There is no method of changing a variable’s target function reference in its current memory location, and causing another variable reference to pick up those changes.
 

Tying it all together

 
What was it I was even trying to do in the first place? I work on a widget library that’s central to the various customer-facing products that we work on within our company. The other Front-end Engineers at my company, who consume our product for use in their own, sometimes have need of events being published that they can subscribe to, at certain places within the widget that don’t currently publish anything. Whenever they find themselves in this situation, they essentially have 2 options: hack at the widget prototype to add an event trigger for their own use, which is bad form, old man; or wait upwards of a month for us to release the next update of our widget library, which may or may not contain this new event trigger, depending on what we had time to do in our previous sprint.

I wanted to be able to give our developers a legitimate method they could use for arbitrarily adding event triggers either before or after any given method that exists within the widget prototype, for any given widget in our library. This is a fairly simple ordeal, in theory, which relies on the same principles as jQuery’s proxy() function:

var handler = $.myNamespace.myWidget.prototype.myFunction;

$.myNamespace.myWidget.prototype.myFunction = function () {
    var returnVal;

    this._trigger("some_arbitrary_event_before");
    returnVal = handler.apply(this, arguments);
    this._trigger("some_arbitrary_event_after");

    return returnVal; // Remember to return the event's outcome for processing by the calling function!
};

This decorates a prototype function with an additional functional layer, which is responsible for broadcasting our custom events. There’s just one problem, though. How do we propagate that functional change out to the rest of the event listeners that are already subscribed to myFunction()? Imagine this living inside myWidget’s prototype definition:

_create: function () {
    // Some widget setup stuff here
    this.element.on('click', $.proxy(this._clickHandler, this));
},

_clickHandler: function (e) {
    console.log("I was clicked!");
}

Now, if we want to be notified with an event anytime _clickHandler() is called, we could wrap that function in our decorator from the example above, but our element’s event data already contains a function pointer that leads directly to _clickHandler(). Just because we’re now wrapping _clickHandler() in an extra layer doesn’t mean the event data reference gets updated to use it. It will fire _clickHandler() directly, and nobody will be alerted to anything. So how can we propagate this change out to everybody that’s already subscribed? That’s right! We can use the jQuery.cache object!

If we loop through the jQuery.cache and search for all entries with an event property, then loop through each of its event definitions, we can compare the guid on those stored function handles to the guid on the function we’re decorating. If they match, we can then overwrite that particular function handle with our new function decorator (and port the guid forward to our new function decorator at the same time), like this:

var self = this,
    handler = this.constructor.prototype[fnName];

this.constructor.prototype[fnName] = function () {
    var returnVal;
    this._trigger("some_arbitrary_event_before");
    returnVal = handler.apply(self, arguments);
    this._trigger("some_arbitrary_event_after");

    return returnVal;
};

this.constructor.prototype[fnName].guid = handler.guid;

$.each($.cache, function (expando, obj) {
    if (obj.events) {
        $.each(obj.events, function (type, events) {
            $.each(events, function (index, event) {
                if (event.handler.guid == self.constructor.prototype[fnName].guid) {
                    event.handler = self.constructor.prototype[fnName];
                }
            }
        });
    }
});

Now we’ve successfully created the ability for any developer to arbitrarily decorate a widget’s internal functions with event triggers that alert them to whatever they need to be need to be alerted to, and updated any existing function pointers to use the decorated versions of those functions at the time they’re created! No more hacking away at the prototype in a behavioral-override file of some kind, and no more having to wait on the platform team to provide those extra event triggers for their use.

I know some people out there are probably chomping at the bit right now, so for the sake of full disclosure, I should present the following information.
 

Caveats!

 
This presents an opportunity for a bad development habit, which is tying your code to a widget’s implementation vs its interface! Naughty naughty! You should be wary of ever tying your code to internal implementations, especially those that should be treated as private! You never know from release to release whether those internal references are going to remain available, or behave the same way they did at the time you decided to decorate them in this fashion!

That being said, you can at least try to mitigate these issues by providing an additional parameter to your function, which will tell it what behavior it should use in the chance that the internal method is no longer available. You could receive a parameter like failSilently, which could be not passed at all (or passed as an explicit false value) in the case that you want your function to throw an Error if the target function isn’t available, for critical functionality that will break if those events are no longer received. You could then pass in an explicit true value in the case of noncritical functionality (e.g. console debugging messages), in which case it could simply return if the target function isn’t available, allowing for graceful degradation.

The above strategy, coupled with robust unit tests (and changelog notes on the part of the publishing author), should be enough to catch the majority of any such fragile implementations before they make it out the door to production.
 

Conclusion

 
I hope you’ve learned a bit more about the guts of jQuery from this post, and how we can use its own internal cache object to our advantage, to provide some extra functionality to those that consume your code in a 3rd party manner. As always, if you have any questions or thoughts in general pertaining to the content, feel free to post them below in the comments section.

Leave a Reply

Your email address will not be published. Required fields are marked *