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.