Skip to content

Instantly share code, notes, and snippets.

@rafialhamd
Created June 11, 2019 09:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rafialhamd/b058737d54e0c29c1f3eefa8cdbe346b to your computer and use it in GitHub Desktop.
Save rafialhamd/b058737d54e0c29c1f3eefa8cdbe346b to your computer and use it in GitHub Desktop.
Alternative ways to pass a callback to a function in Javascript

#Alternative ways to pass a callback to a function in Javascript

Javascript is extremely flexible, encouraging us to find alternatives to almost any possible expression or statement. For example, passing a callback function to another function can be done in different ways.

Because a function is an object in Javascript, it can be passed to another function as an argument and executed later. Callbacks let us defer the code execution to the moment it's really needed.

function waitAndCall(func)
{
  setTimeout(func,parseInt(Math.random()*10000));
}

function doOne()
{/*do something*/}

function doTwo()
{/*do something*/}

function myFunc(param1,param2,callback1,callback2)
{
  waitAndCall(function(){
    callback1();
  });

  waitAndCall(function(){
    callback2();
  });
}

myFunc('a','b',doOne,doTwo);

myFunc emulates the asynchronous behavior. It receives callbacks as arguments and executes them after the function execution. This way of passing callbacks is pretty straightforward and effective. There is no need in additional objects, everything just works. Readability is also good, you can see all your callback functions in the argument list, which also provides us with an additional layer of naming abstraction. I like the simplicity of this method but it is not perfect.

function myFunc(param1,param2,param3,param4,callback1,callback2,callback3,callback4)
{
  waitAndCall(function(){
    callback1();
  });

  waitAndCall(function(){
    callback2();
  });

  waitAndCall(function(){
    callback3();
  });

  waitAndCall(function(){
    callback4();
  });
}

myFunc('a','b','c','d',doOne,doTwo,doTree,doFour);

When a number of callbacks and parameters grows, it starts to be hard to maintain the code. The order of passed arguments must be right, rechecked at all places of program where myFunc is called. Readability also suffers from the long argument list. myFunc now looks complicated, making an impression that it should do some complex job, though it does nothing for now.

Using an object with parameters as a single argument may help in some matter:

function myFunc(args)
{
  waitAndCall(function(){
    args.callback1();
  });

  waitAndCall(function(){
    args.callback2();
  });

  waitAndCall(function(){
    args.callback3();
  });

  waitAndCall(function(){
    args.callback4();
  });
}

myFunc({param1:'one',param2:'two',param3:'three',param4:'four',
  callback1:doOne,callback2:doTwo,callback3:doTree,callback4:doFour});

Now at least we shouldn't keep the right order of passed arguments. But the argument naming abstraction now moved outside of myFunc announcement. The caller now should know the naming of our arguments and keep track of it's changes to pass the correct parameters.

##A glance from a different perspective

If you think carefully about callback mechanics, you may notice that callback is not something affecting the function execution and the returned result. In fact, callback is something function even doesn't need at the time of it's execution.

function myFunc(param1,param2,callback1,callback2)
{
  waitAndCall(function(){
    if(callback1)
      callback1();
  });

  waitAndCall(function(){
    if(callback2)
      callback2();
  });

  return 'c';
}

var r=myFunc('a','b');//return 'c' and continue
var r=myFunc('a','b',doOne,doTwo);//return 'c' and continue, while at some event call doOne and doTwo

You may say that callback extends the function behavior, but function itself can successfully execute without the callback and return the relevant result. Callback may be used later but not necessarily, so, at the time of a function execution, it may not even exist.

Why do we pass a callback to a function as an argument when it doesn't even affect it's execution? Let's try to find another ways to pass a callback to a function without mixing it with data parameters.

One method is about passing callback not before, but after the function execution. We can define an object in the function body, return it to the caller and use it as a shared storage of callbacks.

function myFunc(param1,param2)
{
  var announce={
    callback1:new Function,
    callback2:new Function
  };

  waitAndCall(function(){
    announce.callback1();
  });

  waitAndCall(function(){
    announce.callback2();
  });

  return announce;
}

var myFuncWhen=myFunc();
myFuncWhen.callback1=doOne;
myFuncWhen.callback2=doTwo;

I've been using this method for while and must say it does a job improving code readability. If you need to return some data, you may extend the resulting object. I also used a small upgrade to call more then one function on some event occurrence.

function FunctionStack(contextObj)
{
  var userHandlerStack=[];

  function trigger()
  {
    if(typeof arguments[0]==='function' && userHandlerStack.indexOf(
      arguments[0])===-1)
      userHandlerStack.push(arguments[0]);
    else
      for(var k=0;k<userHandlerStack.length;k++)
        userHandlerStack[k].apply(this,arguments);

    return this;
  }

  return trigger.bind(contextObj||this);
}

function myFunc(param1,param2)
{
  var announce={
    callback1:new FunctionStack(announce),
    callback2:new FunctionStack(announce)
  };

  waitAndCall(function(){
    announce.callback1();
  });

  waitAndCall(function(){
    announce.callback2();
  });

  return {
    value1:'a',
    value2:'b',
    when:announce
  }
}

var r=myFunc('a','b');
r.when.callback1(doOne);
r.when.callback2(doTwo);
r.when.callback1(doThree).callback2(doFour);

FunctionStack produce a scope to accumulate a set of functions and execute them when needed. This is useful when working with many callbacks and chaining is also supported. myFunc now can be called with pure data parameters not messing with callbacks. The method works but now we have a service code inside the function body, return area now looks untidy, method is good but have issues.

##The function context

In Javascript, a function context is an object that function has a reference to, during it's execution. The function object itself doesn't contain a reference to the function context but it is possible to pass any context to the function using call,apply or bind methods.

Let's return to callbacks and another repeating pattern. While using myFunc in different places of a program, you may notice that very often various data parameters are paired with same callbacks.

myFunc('a','b',doOne,doTwo);

//...

myFunc('c','d',doOne,doTwo);

//...

myFunc('e','f',doOne,doTwo);

It would be useful to configure a function before it's usage.

var myFuncConfigured=myFunc.config({callback1:doOne,callback2:doTwo});

//...

myFuncConfigured('a','b');

//...

myFuncConfigured('c','d');

//...

myFuncConfigured('e','f');

We can utilize a function context for this purpose.

function myFunc(param1,param2)
{
  waitAndCall(function(){
    this.callback1();
  }.bind(this));

  waitAndCall(function(){
    this.callback2();
  }.bind(this));
}

var myFuncBound=myFunc.bind({callback1:doOne,callback2:doTwo});
myFuncBound('a','b');

This looks usable but now we can't bind other context to myFunc. Still, I like the concept of using this as a transporter of callbacks into the function so I will develop this idea. Looking at the myFunc.bind code, it's not clear enough, what is the code about. We want to pass some callbacks to the function, so lets create an alias of bind function called 'when' to make it clear.

Function.prototype.when=function(callbacks){
  return this.bind({announce:callbacks})}

function myFunc(param1,param2)
{
  waitAndCall(function(){
    this.announce.callback1();
  }.bind(this));

  waitAndCall(function(){
    this.announce.callback2();
  }.bind(this));
}

var myFuncReady=myFunc.when({callback1:doOne,callback2:doTwo});
myFuncReady('a','b');

With less of code, we create a simple method to pass callback to a function without messing it with arguments. Method when can be used only once, because bind doesn't assign a new context to a function that was bound before. If we do myFunc.bind(a).bind(b), the resulting function context will be a object, assignment of b object will be ignored. Unfortunately, there is no way to access a function execution context from outside of function body before it's execution. But it is possible to utilize function object to keep a new context that can be assigned using bind method. This trick may help us make possible to call when multiple times with different callbacks.

Function.prototype.when=function(callbackName,callback){
  var context,bound;
  context=this.context||{announce:{}};
  bound=this.bind(context);
  bound.context=context;
  context.announce[callbackName]=callback;
  return bound;
}

function myFunc(param1,param2)
{
  waitAndCall(function(){
    if(this.announce)
      this.announce.callback1();
  }.bind(this));

  waitAndCall(function(){
    if(this.announce)
      this.announce.callback2();
  }.bind(this));
}

var myFuncReady=myFunc.when('callback1',doOne).when('callback2',doTwo);
myFuncReady('a','b');

At the first when execution, a new context is created and bound to the new function. It is also stored in the function object property to be mutated on every next when call. The next version of when is able to assign multiple callback functions to a single callback/event name.

(function(){
  function announce(callbackName)
  {
    var o,r;
    o=Array.prototype.slice.call(arguments,1);
    if(r=this.context.announce.callbacks[callbackName])
      r.forEach(function(callback){
        callback.apply(window,o)});
  }

  function when(callbackName,callback)
  {
    var l,e,m;

    e=this.context||{};

    if(!e.announce){
      e.announce=announce.bind({context:e});
      e.announce.callbacks=[];
    }

    l=e.announce.callbacks;
    if(!l[callbackName])
      l[callbackName]=[];
    if(l[callbackName].indexOf(callback)===-1)
      l[callbackName].push(callback);

    m=this.bind(e);
    m.context=e;
    return m;
  }

  Object.defineProperty(Function.prototype,'when',{configurable:true,
    enumerable:false,writable:true,value:when})
})()

function myFunc(param1,param2)
{
  waitAndCall(function(){
    if(this.announce)
      this.announce('callback1', 'a', 'b');
  }.bind(this));

  waitAndCall(function(){
    if(this.announce)
      this.announce('callback2', 'c', 'd');
  }.bind(this));
}

var myFuncReady=myFunc.when('callback1',doOne).when('callback2',doTwo);
myFuncReady.when('callback1',doThree).when('callback2',doFour);
myFuncReady('a','b');

Hope this little dive into callbacks management will inspire you for a new quality code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment