Overloading Javascript Functions Using A Sub-Function Approach
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.
Want to use code from this post? Check out the license.
Reader Comments
I think you meant looser languages. PHP is the loser language :P
This is cool!
@Eric,
Ha ha ha, thanks for the critical catch - this has now been corrected.
@Pradeep,
Thanks, I'm glad you like it.
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?
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.
@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.
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?
@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.
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.
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.
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.
@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.
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
@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.
I wanted to take my other post on animate-powered easing and augment it to use two different signatures: one with "duration", one without:
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.