jQuery: Extending bind() to define context of execution (scope)

February 05, 2009

One of the main advantages of jQuery is that it does a lot of thinking for you, especially with more complex things like event handling. Unfortunately, as you move into more advanced application building, the jQuery developer is left without a standard method of applying context to an event.

Although creating a generic bind function to use the Function object’s call or apply methods is always an option, it suffers from two problems:

  • Not integrated with the library, which can cause developer confusion and creates hard to read code
  • Unbinding that handler becomes problematic without additional bookkeeping

While many have called for scope of execution to be added to bind, you can simply extend the functionality yourself with the following:

function() {
    jQuery.extend(jQuery.event, {
        add: function(elem, types, handler, data, scope) {
            if ( elem.nodeType == 3 || elem.nodeType == 8 )
                return;

            // For whatever reason, IE has trouble passing the window object
            // around, causing it to be cloned in the process
            if ( elem.setInterval && elem != window )
                elem = window;

            // Make sure that the function being executed has a unique ID
            if ( !handler.guid )
                handler.guid = this.guid++;

            // if data or scope is defined, use a proxy function
            if( data !== undefined || scope !== undefined) {
                // Create temporary function pointer to original handler
                var fn = handler;

                // Create unique handler function, wrapped around original handler
                if ( scope !== undefined) {
                    // Create proxy function to apply correct context determined by scope
                    var proxyFn = function(){ return fn.apply(scope, arguments); };
                    handler = this.proxy( fn, proxyFn );
                } else {
                    handler = this.proxy( fn );
                }
            }

            // if data is passed, bind to handler
            if ( data !== undefined ) {
                // Store data in unique handler
                handler.data = data;
            }

            // Init the element's event structure
            var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
                handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
                    // Handle the second event of a trigger and when
                    // an event is called after a page has unloaded
                    return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
                        jQuery.event.handle.apply(arguments.callee.elem, arguments) :
                        undefined;
                });
            // Add elem as a property of the handle function
            // This is to prevent a memory leak with non-native
            // event in IE.
            handle.elem = elem;

            // Handle multiple events separated by a space
            // jQuery(...).bind("mouseover mouseout", fn);
            jQuery.each(types.split(/\s+/), function(index, type) {
                // Namespaced event handlers
                var namespaces = type.split(".");
                type = namespaces.shift();
                handler.type = namespaces.slice().sort().join(".");

                // Get the current list of functions bound to this event
                var handlers = events[type];

                if ( jQuery.event.specialAll[type] )
                    jQuery.event.specialAll[type].setup.call(elem, data, namespaces);

                // Init the event handler queue
                if (!handlers) {
                    handlers = events[type] = {};

                    // Check for a special event handler
                    // Only use addEventListener/attachEvent if the special
                    // events handler returns false
                    if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem, data, namespaces) === false ) {
                        // Bind the global event handler to the element
                        if (elem.addEventListener)
                            elem.addEventListener(type, handle, false);
                        else if (elem.attachEvent)
                            elem.attachEvent("on" + type, handle);
                    }
                }

                // Add the function to the element's handler list
                handlers[handler.guid] = handler;

                // Keep track of which events have been used, for global triggering
                jQuery.event.global[type] = true;
            });

            // Nullify elem to prevent memory leaks in IE
            elem = null;
        }
    });

    jQuery.fn.extend({
        bind: function( type, data, fn, scope ) {
            return type == "unload" ? this.one(type, data, fn, fn && scope) : this.each(function(){
                jQuery.event.add( this, type, fn || data, fn && data, (fn && data) && scope );
            });
        }
    });
}();

As you can see, you simply use bind as you always have, but, if you wish to define scope, pass the context in as the last parameter. You ARE required to pass something for data with this method. If you have nothing to pass, simply send an empty object: {}. The bookkeeping is done by jQuery, and you should be able to unbind without any headaches.

Please note: This isn’t fully tested, but it’s based off changes I made elsewhere. I will update this later if I find issues with it.