Saturday, July 7, 2012

JavaScript Method Overloading

I'm focusing on leveling-up my JavaScript skills this month. To start, I grabbed the "early" edition of an in-progress book by John Resig and Bear Bibeault: Secrets of the JavaScript Ninja. (I put "early" in quotes, since I now have v.10 of the MEAP and it has been in progress since 2008, but is still actively being worked on.)

Since it is still in progress, it is a bit rough around the edges, but so far (I'm on chapter 5 now) I've found it to be a solid presentation of the nuances of JavaScript. If you are an intermediate level JS developer and want to take solid steps to becoming more expert, I can recommend this as one of the books you should read. It focuses on JavaScript itself, not some of the myriad JavaScript frameworks and libraries that have come out in the JavaScript Cambrian explosion of the last few years - though it will analyze techniques used in Prototype and jQuery.


/* ---[ JavaScript method overloading ]--- */

Today I have a short note on JavaScript method overloading. Technically, there is no such thing as overloading in JavaScript - you can have only one function for a given function name. JavaScript, being flexible, allows you to pass in too few or too many arguments to a function. There are ways to handle that within your function with a series of if/else blocks, but you might want something more formal and less noisy.

The Secrets book suggests doing it this way:

function addMethod(object, name, fn) {
  var old = object[name];
  object[name] = function(){
  if (fn.length == arguments.length)
    return fn.apply(this, arguments);
  else if (typeof old == 'function')
    return old.apply(this, arguments);
  else throw "Wrong number of args";  // I added this part
  };
}

This method layers each version on top of the other, like a semi-recursive set of closures. The last one is the fallback for the previous one.

Let's try it out:

var ninja = {};

addMethod(ninja, 'attack', function() {
  console.log("Attack no args");
});
addMethod(ninja, 'attack', function(x) {
  console.log("Attack 1 person: " + x);
});
addMethod(ninja, 'attack', function(x, y) {
  console.log("Attack 2 people: " + x + ", " + y);
});

ninja.attack();
ninja.attack("Groucho");
ninja.attack("Groucho", "Harpo");
ninja.attack("Groucho");
ninja.attack();
try {
  ninja.attack("Groucho", "Harpo", "Chico");
} catch (e) {
  console.log("ERROR: attack: " + e);
}

If I run this on the command line with node.js, I get:

Attack no args
Attack 1 person: Groucho
Attack 2 people: Groucho, Harpo
Attack 1 person: Groucho
Attack no args
ERROR: attack: Wrong number of args

Pretty cool technique. It also doesn't matter the order in which you define the overloads. Try it out by changing the order of the addMethod calls.


/* ---[ Alternative version: my contribution ]--- */

To polish up my JS ninja skills, I came up with another way to do this:

function overload(object, name, fn) {
  object[name] = object[name] || function() {
    if (object[name + "." + arguments.length]) {
      return object[name + "." + arguments.length].apply(this, arguments);
    }    
    else throw "Wrong number of args"
  };

  object[name + "." + fn.length] = fn;
}

Here I make the primary function name ("defend") a function that invokes "unpublished" functions that the overload function created.

So if you call ninja.defend("a", "b"), it gets routed to ninja[defend.2]("a", "b"). I have to use the bracket notation, since ninja.defend.2() will try defend a method called "2" on the defend method, which of course doesn't exist.

To prove it works the same, let's run this with node.js:

var ninja = {};

overload(ninja, 'defend', function(x, y) {
  console.log("Defense against 2 attackers: " + x +", "+ y);
});
overload(ninja, 'defend', function(x) {
  console.log("Defense against 1 attacker:  " + x);
});
overload(ninja, 'defend', function() {
  console.log("General Defense");
});

ninja.defend();
ninja.defend("Moe");
ninja.defend("Moe", "Larry");
ninja.defend("Moe");
ninja.defend();
try {
  ninja.defend("Moe", "Larry", "Curly");
} catch (e) {
  console.log("ERROR: defend: " + e);
}  

Output:

General Defense
Defense against 1 attacker:  Moe
Defense against 2 attackers: Moe, Larry
Defense against 1 attacker:  Moe
General Defense
ERROR: defend: Wrong number of args

Lastly, let's call one of those unpublished methods directly to prove that is what it's doing under the hood:

var ninja = {};

overload(ninja, 'defend', function(x) {
  console.log("Defense against 1 attacker:  " + x);
});

ninja["defend.1"]("Moe");

Gives the output:

Produces:

Defense against 1 attacker:  Moe