Callback functions are used in JavaScript (JS) extensively. They are required for AJAX/XHR, event handling, and in popular JS libraries. However, passing around functions as arguments can cause headaches, especially for beginner JS programmers.
Problems with binding/this
One of the biggest problems with passing functions as callbacks is this and what it
refers to. You usually run into the problem when passing an object’s method as a
callback function. Another way to encounter this problem is by passing as a callback a function that
expects to be bound to a certain type of object. You’ll usually get an error stating that
this.someProperty is undefined. You can resolve this with
Function.apply, Function.call or a closure. For example:
//using Function.call
takesCallback(
function callbackProxy(argOne, argTwo){
//assume myFunction is already defined
myFunction.call(myObject, argOne, argTwo);
}
);
//or using Function.apply
takesCallback(
function callbackProxy(argOne, argTwo){
//assume myFunction is already defined
myFunction.apply(myObject, [argOne, argTwo]);
}
);
//or using a closure
takesCallback(
function callbackProxy(argOne, argTwo){
//assume myObject is already defined
myObject.myMethod(argOne, argTwo);
}
);
In the code above, the function takesCallback takes a callback function as its
only argument and calls that function at a later time with two arguments. The function, callbackProxy is
a wrapper function that takes the arguments and passes them to the real callback, while ensuring
that the function is bound to the correct object.
Problems with Arguments
Another problem with callback functions is that the arguments passed to the callback function are often out of your control. Sometimes you need more information than what is passed to your callback function by the caller. This, too can be solved with a closure. For example:function main(){
var myVarOne = "one";
takesCallback(
function callbackProxy(argOne){
//assume myFunction is alread defined
myFunction(argOne, myVarOne);
}
);
}
Again, the callbackProxy function takes care of calling the real callback with
the correct arguments. In this case it appends myVarOne to the list of arguments.
Problems with references
The issue with references does not come up often. But when it does, it can be difficult to solve. When you use a callback proxy to pass additional arguments to the real callback, you may find that the values of those arguments have changed between the time the callback proxy was created and when it was called. This will happen if you are using a loop counter as an argument for your callback or if you change the value of the variable for some other reason. For example:
//looping
for(var i = 0; i < 5; i++){
takesCallback(function callbackProxy(){
//assume myFunction is already defined
myFunction(i);
})
}
//general example
var x = 2;
takesCallback(function callbackProxy()){
myFunction(x);
}
x = x * 2;
In both of the cases above, when myFunction is called, the value of the first
argument will be 4. This is true for all iterations of the loop. It happens because the references to i and x
are never broken. In the general example, you can eliminate this problem by using another variable
in place of x. In the looping example, the fix is more complicated:
for(var i = 0; i < 5; i++){
takesCallback(
(function breakReference(arg){
return function callbackProxy(){
myFunction(arg);
};
})(i)
);
}
This code creates the callback proxy with the breakReference function. The
breakReference function is executed immediately and returns the callback proxy.
The reference to i is broken because i is passed as an argument.
In JS, primitives passed as arguments are passed by value and objects passed as arguments are passed by reference.
So, if you are trying to break a reference to an object, this method will not work.
A better solution
You probably don’t want to remember all these tricks or write these routines over and over. Personally, I dislike closures because they can become nested several levels deep, making debugging and reading your code difficult. If you’ve ever written a web application, you know the code can quickly become a mess. Closures can compound that mess, so I try to stay away from them or hide them in other functions. So what can you do? You can write a function to take care of these issues for you and use it as often as you need or you can use a binding function from your favorite JS library. Or, you can use the one I already wrote (if you don’t use a library and don’t want to write your own).
/**
* @param {Function} func the callback function
* @param {Object} opts an object literal with the following
* properties (all optional):
* scope: the object to bind the function to (what the "this" keyword will refer to)
* args: an array of arguments to pass to the function when it is called, these will be
* appended after any arguments passed by the caller
* suppressArgs: boolean, whether to supress the arguments passed
* by the caller. This default is false.
*/
function callback(func,opts){
var cb = function(){
var args = opts.args ? opts.args : [];
var scope = opts.scope ? opts.scope : this;
var fargs = opts.supressArgs === true ?
[] : toArray(arguments);
func.apply(scope,fargs.concat(args));
}
return cb;
}
/* A utility function for callback() */
function toArray(arrayLike){
var arr = [];
for(var i = 0; i < arrayLike.length; i++){
arr.push(arrayLike[i]);
}
return arr;
}
The callback function is fairly straight forward. It takes a function and
and an object literal and returns a callback function that takes care of the
problems discussed earlier. The properties of the object literal are all optional and will
default to what is normal behavior for callbacks. The object literal defines the scope and extra arguments. It can
also supress the arguments provided by the caller (the extra arguments are still used). This is useful when you don’t need the
arguments provided by the caller or when you are using a function that isn’t always called as a callback. Here is
an example of how to use it.
takesCallback(callback(myFunction,{scope:myObject,args:[myArgOne,myArgTwo],suppressArgs:true}));
//or
takesCallback(myObject.myMethod,{scope:myObject,args:[myArgOne]});
As you can see, using the callback function is much easier than using the tricks shown above.
If you are using callbacks often, as is the case with event handling and AJAX, this is very handy tool. You can
create your objects, methods, and functions once and quite easily reuse them.
Pingback: Arrastando elementos com JavaScript (ou JavaScript Drag) KISS - Keep it simple, stupid
Have been reading a bunch of articles on object scope in javascript – was particularly interested in the ajax-callback situations – this article helped me get around a problem I was grappling with – thanks!
- Mahul
Thank you, this is very useful. I think you have a minor typo – your usage example uses the ‘scope’ argument, but the ‘callback’ function expects an argument named ‘bind’.