Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with:

Overloading Javascript Functions Using A Sub-Function Approach

By Ben Nadel on

I've been looking through a lot of jQuery source code lately and one of the things that I see being done all over the place is function overloading. Function overloading is the practice in which a function can take different sets of arguments. In a strict language like Java, overloaded functions are typically defined with physically different method signatures; in looser languages like ColdFusion and Javascript - where you can't define parallel variables with the same name - function overloading is typically done through argument inspection. I wondered, however, if we could use Function behavior in Javascript to create a "best of both worlds" type solution.

In Javascript, Functions are objects; granted, they are very special objects that can be used in conjunction with the "()" operator. But, just as any other objects in Javascript, Functions can have properties associated with them. I wanted to see if we could use these function-level properties to create multiple function signatures that all existed under the same function name.

To see what I'm talking about, I've created a function, randRange(), that can take the following method signatures:

  • randRange( max )
  • randRange( min, max )

In the first invocation, the min is assumed to be zero. In the second invocation, there is no need for assumption as both limits are supplied. Using function-level properties, I am going to define the above two functions using completely different functions off of the core randRange() object:

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Overloading Javascript Functions - Sub-Function Approach</title>
  • <script type="text/javascript">
  •  
  • // I am the core randRange() function who's signature can
  • // be overloaded with a variable number of arguments.
  • function randRange(){
  • // Check to see how many arguemnts we have in order to
  • // determine which function implementation to invoke.
  • if (arguments.length == 2){
  •  
  • // Two-parameters avialble.
  • return(
  • randRange.twoParams.apply( this, arguments )
  • );
  •  
  • } else {
  •  
  • // One-parameters available.
  • return(
  • randRange.oneParams.apply( this, arguments )
  • );
  •  
  • }
  • }
  •  
  •  
  • // I am the single-argument implementation. Notice that
  • // I am a property of the core function object.
  • randRange.oneParams = function( max ){
  • // We are going to assume that the min is zero - pass
  • // control off to the two-param implementation.
  • return( randRange.twoParams( 0, max ) );
  • };
  •  
  •  
  • // I am the double-argument implementation. Notice that
  • // I am a property of the core function object.
  • randRange.twoParams = function( min, max ){
  • return(
  • min +
  • Math.floor( Math.random() * (max - min) )
  • );
  • };
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Try a few different approaches.
  • console.log( "One: ", randRange( 10 ) );
  • console.log( "One: ", randRange( 50 ) );
  • console.log( "One: ", randRange( 100 ) );
  •  
  • console.log( "Two: ", randRange( 100, 110 ) );
  • console.log( "Two: ", randRange( 100, 150 ) );
  • console.log( "Two: ", randRange( 100, 200 ) );
  •  
  • </script>
  • </head>
  • <body>
  • <!-- Intentionally left blank. -->
  • </body>
  • </html>

When we run this code, we get the following console output:

One: 6
One: 18
One: 44
Two: 109
Two: 143
Two: 138

As you can see in the above code, I am defining the randRange() function. But then, I am defining the single and double parameter implementations as properties off of the core randRange() object:

randRange()
randRange.oneParams = function( max )
randRange.twoParams = function( min, max )

Now, the individual method signatures don't have to worry about any kind of arguments-based logic; all the routing logic is factored out and encapsulated within the core randRange() method. This feels like a really clean separation of concerns that leaves the final implementations extremely focused and easy to understand.

In this particular demo, my routing logic depends only on the number of arguments. You could easily augment this, however, to include type checking for methods using the same number of arguments. You could even use the core method to transform several different signatures into one, unified invocation. In any case, I think the factoring-out of argument-specific logic feels really good.




Reader Comments

@Eric,

Ha ha ha, thanks for the critical catch - this has now been corrected.

@Pradeep,

Thanks, I'm glad you like it.

Reply to this Comment

Another way of doing it (shorter version) -

function randRange(){
(randRange[arguments.length] || randRange[2]).apply(this, arguments);
}
randRange[0] = function () {
alert('Error: No parameters passed!');
return 0;
};
randRange[1] = function (max) {
return( randRange[2]( 0, max ) );
};
randRange[2] = function (max, min) {
return(
min +
Math.floor( Math.random() * (max - min) )
);
};

randRange();
randRange(1);
randRange(1, 2);
randRange(1, 2, 3); // calls the no-param version

Anyone see any problems using this approach?

Reply to this Comment

Oops .. the comment in the last line of code above should read "calls the 2-param version" .. which I think is a better implementation considering that the randRange() function essentially wants to deal with maximum of 2 arguments .. any more should be ignored.

Reply to this Comment

@All,

Besides number of arguments, there's also overloading by type of arguments. jQuery("a[name]") does one thing, jQuery(this) does something else and jQuery(function(){}) does something else.

This sort-of argues in favor of defining a hash of subfunctions, doesn't it? With a 2 dimensional hash and the typeof operator, you could deal with the combinatorial explosion of multiple argument types quite naturally:

subfuncs["boolean"]["boolean"]
subfuncs["boolean"]["string"]
subfuncs["boolean"]["number"]
subfuncs["string"]["number"]
subfuncs["string"]["boolean"]
etc.

It's like Java signatures, but managed out of a hash.

Nice how you get people thinking, Ben.

Reply to this Comment

A (very) quick prototype of a cleaner way of doing (strict) arguments check for both - type and count -

Function.prototype.overload = function () {
this.variants = this.variants || {};
var len = arguments.length, args = (Array.prototype.slice.call(arguments)),
id = args.slice(0,len-1).join(',');
this.variants[id] = this.variants[id] || args[len-1];
};
Function.prototype.overloaded = function () {
var len = arguments.length, args = (Array.prototype.slice.call(arguments)),
id = [];
for (var i=0, len=args.length; i<len; i++) {
id.push(typeof(args[i]));
}
id = id.join(',');
var fn = randRange.variants[id];
if (randRange.variants && fn) {
fn.apply(fn, arguments);
}
};
function randRange(){
randRange.overloaded.apply(randRange, arguments);
}

randRange.overload(
'string', 'boolean', 'number',
function (mystr, mybool, mynum) {
alert(['String:'+mystr, 'boolean:'+mybool, 'Number:'+mynum].join('\n'));
}
);

randRange.overload(
'string', 'number', 'boolean',
function (mystr, mynum, mybool) {
alert(['String:'+mystr, 'Number:'+mynum, 'boolean:'+mybool].join('\n'));
}
);

randRange('abc', 100, true);
randRange('pqr', false, -1);
randRange(false, 'str', -1); // no "variant" matches .. call ignored.

This can also be enhanced to take in metadata about arguments like "mandatory/optional", default values, etc. -

randRange.overload(
'string[Default Value]', 'number:-1', 'boolean:optional',
function (mystr, mynum, mybool) {
...
}
);

What do you guys think?

Reply to this Comment

@EtchEmKay, @Steve,

These are some very interesting ideas. This really is like moving back to a strict method signatures. I have to run to catch a plane, but I'll let this sink in a bit. Some very clever stuff going on here.

Reply to this Comment

I've done this a couple times in cfscript, when I wanted a conditional argument. I've also done something similar in cfc methods when a function contains most of the logic I want already, but I want to interact with it in different ways.

The advantage to cfc methods is that you can self document a bit better and if you choose you can use named arguments in your calls to tell "the next guy" what you're doing.

Reply to this Comment

This is really cool - I love overloading and overriding in Javascript.

Just looking at some of the suggestions - you could also put the functions in an array, and then call the function based on the arguments.length which would be in the corresponding place in the array of functions...

function randRange(){
return randRange.Params[arguments.length-1].apply(this, arguments)
}
randRange.Params = [
function( max ){
return( randRange.Params[1]( 0, max ) );
},
function( min, max ){
return(min + Math.floor( Math.random() * (max - min) ))
}
];

This obviously has a little problem with error handling - ie: when there's no arguments - but that's easy enough to cater for.

Reply to this Comment

So was the idea purely to work out a clean separation of functional intent for overloading? Otherwise why not keep is simple like so?

function randRange(arg1, arg2)
{
if (arg1 === undefined && arg2 === undefined) { return; }
else if (arg2 === undefined) { return (0 + Math.floor(Math.random() * (arg1 - 0))); }
else { return (arg1 + Math.floor(Math.random() * (arg2 - arg1))); }
}

Just wondering. Still a fun read as always Ben.

Reply to this Comment

@Grant,

Yeah, ColdFusion's ability to use both ordered and named arguments is something that opens up a lot of options for us.

@Wayne,

Very true; also, something I hadn't thought of before is that you should be able to just call the method recursively. Meaning, the one-param method doesn't have to call the two-param method directly; rather, it can simply call the core, randRange() method and the core method can take care of re-routing to the two-param handler. It adds a bit more processing, but it might be a cleaner implementation??

@Adam,

Yes, I was really just trying to factor out the routing logic. In this kind of example, the difference is negligible; but, the internal behavior of a function can change dramatically depending on the arguments. In such a case, I think it will be quite nice to not have to worry about branching logic within a large function. When I get back to NY (I'm at #BFLEX right now), I'll come up with a better example.

Reply to this Comment

Nice thoughts on the subject,
A good use-case for overloading is when you are dealing with google maps, making a constructor for the market with assorted options (lat, lng, icon, text, function)

Seem to recall reading a post by jresig on the array approach, along the lines of the http://ejohn.org/apps/learn/#90 example

Lookimg forward to your further exploration

Reply to this Comment

@Atleb,

Cool post by Resig. He's using like some sort of Wrapper pattern where each layer defines a different method signature (and then passes the control off to a deeper functional layer if the current signature doesn't match).

I have an idea for an example, but it will have to wait till Monday.

Reply to this Comment

I wanted to take my other post on animate-powered easing and augment it to use two different signatures: one with "duration", one without:

http://www.bennadel.com/blog/2010-An-Example-Of-Overloaded-Functions-With-Very-Different-Sub-Function-Implementations.htm

The post isn't really about easing, but it *is* an example of how factoring-out the branching logic of overloaded functions can create an important separation of concerns that leads to a clean, cohesive execution.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.