21 August 2008

Function overloading in JavaScript

Trying to write a function so that it will handle different numbers and types of argument always seems to take a lot more code that it should. Today I ended up with some nightmarish if...else... because I wanted to support:

  • Object, Object
  • Object, String
  • Object, Object, String

I remembered a post by John Resig from a while back on this, however his method doesn't handle different types. A quick Google found this snippet which does the job but seems a bit clunky, converting constructors to strings and the like. Satisfied that it could probably be improved upon here is my go at it.

Implementation

Here an Impl class holds the different method implementations against their signatures, defined as an array of constructors. The add method maps a new signature to an implementation, the exec method identifies the correct implementation for the current arguments and executes it and the compile method returns a function that can be used to overwrite the "daddy" function.

function Impl(){
 var _impl = {};
 return {
  add: function(aSignature, fImpl){
   _impl[aSignature] = fImpl;
  },
  exec: function(args, scope){
   var aArgs = Array.prototype.slice.call(args);
   var aCtors = [];
   for(var i in aArgs) aCtors.push(aArgs[i].constructor);
   return (_impl[aCtors] || function(){}).apply(scope, aArgs);
  },
  compile: function(){
   var impl = this;
   return function(){
    return impl.exec(arguments, this);
   };
  }
 };
}

Usage

Here's an example setup of a function stringify that will take two strings, two numbers or an array and do different things depending on what arguments are passed.

function stringify(){
 var someVar = 'Items: ';
 var impl = new Impl();
 
 impl.add([String, String], function(sLeft, sRight){
  return sLeft + ': ' + sRight;
 });
 impl.add([Number, Number], function(iLeft, iRight){
  return 'Sum: ' + (iLeft + iRight).toString();
 });
 impl.add([Array], function(aIn){
  return someVar + aIn.join(', ');
 });
 
 stringify = impl.compile();
 return stringify.apply(this, arguments);
}

Each implementation is added with an array of constructors e.g. [Number, Number] representing the arguments signature to match and the implementation function. Notice that we don't need to do any type checking of the variables in the implementation function, as we have to have matched the signature to get that far, and we can give then relevant names.

Notice the last two lines are:

stringify = impl.compile();
return stringify.apply(this, arguments);

So on the first execution of the function it is overwritten with a "compiled" version and then that version is called on the next line. Subsequent calls to the function will go straight to the "compiled" version thus speeding things up. This could also be done without the compilation and overwriting bit by just calling exec directly thus:

return impl.exec(arguments, this);

Calling our function

stringify('foo', 'bar'); // => 'foo: bar'
stringify(26, 5); // => 'Sum: 31'
stringify( [ 1, 2, 3 ] ); // => 'Items: 1, 2, 3'

Our different numbers and types of argument are giving us the output we'd expect.

4 comments:

Corban Brook said...

I love your implementation. We are running into areas where we may need this in the processing.js project.

We translate processing Java into javascript and need a clean solution for constructor overloading.

Would love to talk to you more about this. we are at irc://irc.mozilla.org/processing.js

Corey said...

Derek,

I have been trying to play around with your implementation, but I cant seem to figure out why when I move it to an external javascript file, all the methods return undefined, rather that the string. Any ideas?

Also do you have any idea how this technique compares to the one posted by John when it comes to performance?

Corey said...

I figured out the problem. I had a little test script where I extended one of the javascript base types, and thus caused an issue of having an undefined constructor return

Corey said...

I figured out the problem. I had a little test script where I extended one of the javascript base types, and thus caused an issue of having an undefined constructor return