In a side project that I’ve been working on I built a quick-and-dirty function for doing simple method overloading. For those of you who aren’t familiar with, it’s just a way of mapping a single function call to multiple functions based upon the arguments they accept.
Here’s the function in question:
// addMethod - By John Resig (MIT Licensed) 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 ); }; }
and here is how you might use it:
function Users(){ addMethod(this, "find", function(){ // Find all users... }); addMethod(this, "find", function(name){ // Find a user by name }); addMethod(this, "find", function(first, last){ // Find a user by first and last name }); }
Or, if you wanted to use it with an object prototype:
function Users(){} addMethod(Users.prototype, "find", function(){ // Find all users... }); addMethod(Users.prototype, "find", function(name){ // Find a user by name }); addMethod(Users.prototype, "find", function(first, last){ // Find a user by first and last name });
And here’s what the end result would look like to the user:
var users = new Users(); users.find(); // Finds all users.find("John"); // Finds users by name users.find("John", "Resig"); // Finds users by first and last name users.find("John", "E", "Resig"); // Does nothing
Obviously, there’s some pretty big caveats when using this:
- The overloading only works for different numbers of arguments – it doesn’t differentiate based on type, argument names, or anything else. (ECMAScript 4/JavaScript 2, however, will have this ability – called Multimethods – I’m quite excited.)
- All methods will some function call overhead. Thus, you’ll want to take that into consideration in high performance situations.
Now, the secret sauce is all going back to the fn.length
expression. This isn’t very well known, but all functions have a length property on them. This property equates to the number of arguments that the function is expecting. Thus, if you define a function that accepts a single argument, it’ll have a length of 1, like so:
(function(foo){}).length == 1
I did some basic testing and this technique seems to work in all modern browsers – please let me know if I’m wrong.
If you’re concerned about adding a function call overhead when binding only a single function, then you can give this version of addMethod a try:
// addMethod - By John Resig (MIT Licensed) function addMethod(object, name, fn){ var old = object[ name ]; if ( old ) object[ name ] = function(){ if ( fn.length == arguments.length ) return fn.apply( this, arguments ); else if ( typeof old == 'function' ) return old.apply( this, arguments ); }; else object[ name ] = fn; }
That one will attach the first bound function with no additional checks – keeping it nice and speedy. Once extra functions are bound, things will slow ever so slightly.
This, also, has the added benefit of being able to bind default “catch all” functions which will handle all the calls that pass through. The result would look something like this:
var users = new Users(); users.find(); // Finds all users.find("John"); // Finds users by name users.find("John", "Resig"); // Finds users by first and last name users.find("John", "E", "Resig"); // Finds all
This function won’t change the world, but it’s short, concise, and uses an obscure JavaScript feature – so it wins in my book.
Stuart Langridge (November 13, 2007 at 3:32 am)
“Short, concise, and uses obscure JavaScript features” — is that your epitaph? :-)
Dr Nic (November 13, 2007 at 7:04 am)
That’s just lovely!
Wladimir Palant (November 13, 2007 at 7:19 am)
I generally do worry about call overhead – which is why I would avoid chaining up overloaded handlers (exception throwing can be removed in release):
function addMethod(object, name /*, function, ...*/)
{
var map = [];
if (typeof object[name] == "function")
map[-1] = object[name];
for (var i = 2; i < arguments.length; i++)
{
if (typeof arguments[i] != "function")
throw "Parameter " + (i+1) + " should be a function";
if (arguments[i].arity in map)
throw "More than one function for " + arguments[i].arity + " parameters defined";
map[arguments[i].arity] = arguments[i];
}
object[name] = function() {
if (arguments.length in map)
return map[arguments.length].apply(this, arguments);
else if (-1 in map)
return map[-1].apply(this, arguments);
else
throw "This method doesn't support " + arguments.length + " parameters";
}
}
And would use the method like this:
addMethod(Users.prototype, "find", Users.prototype.findByName, Users.prototype.findByFirstAndLastName);
The original Users.find() method would stay a catch-all here as well.
she (November 13, 2007 at 7:22 am)
hmm javascript syntax isnt as nice as Ruby syntax ;)
John DeHope (November 13, 2007 at 9:23 am)
It’s like scripting an object model! Can we also add private fields with public setters and getters too?
function addField(object, name, initialValue)
{
if ( ! object.private ) object.private = {};
object.private[name] = { name: name, initialValue: initialValue };
}
function addGetter ( object, name, fn )
{
if ( ! fn ) fn = function(){return this.private[name].value;};
object['get' + name] = fn;
}
function addSetter ( object, name, fn )
{
if ( ! fn ) fn = function(value){this.private[name].value = value;};
object['set' + name] = fn;
}
I wrote this ad hoc so I hope it doesn’t make me look totally stupid. But I think you get my drift. By scripting all the object model definitions, we can add extra metadata, and also put hooks in strategic places.
Johan Sundström (November 13, 2007 at 11:42 am)
function.length is cross browser dependable for user functions, but not for native functions, particularly those which already support different argument signatures (like prompt). So it is more useful for this purpose than currying.
Blair Mitchelmore (November 13, 2007 at 2:15 pm)
Fancy John,
It reminds me of the idea I had for jQuery plugins where the types and length of the arguments defined which plugin to call for plugins with the same name. Not practical with ES3 but in jQuery for ES4 it would be very snazzy.
John Resig (November 13, 2007 at 11:27 pm)
@Wladimir: I did some performance testing of your code (and mine) with some interesting results. Yours consistently takes 20ms to do a 1000 method calls – which is pretty good. In my addMethod the speed can vary based upon the number of methods bound. For example, if the last bound method is called then it takes about 10ms – if 4 methods are bound and the first-bound one is called it takes about 50ms. So for about 1-3 bound methods it’s optimal to use mine, beyond that, it’s optima to use yours. However, mine has a slight advantage since it’s able to be optimized by the user (having the most-frequently called method being bound last).
@John DeHope: This particular functionality already exists in Firefox, Opera, and Safari – called Getters and Setters (I wrote a blog post detailing their functionality, as well). Hope this helps!
alek (November 14, 2007 at 10:47 am)
I have read that using the identity operator saves a bit of processing because there is no check to see if type conversion is needed. In addMethod length is always number and typeof is always string, would === get you any speed increase?
John Resig (November 14, 2007 at 12:01 pm)
@alek: Good point – I assume that it would provide a minimal speed improvement – but not much in the scheme of things, I wouldn’t think.
Ric (November 14, 2007 at 12:16 pm)
John,
It seems I have seen this technique elsewhere – perhaps a variation of Dean Edwards? I think the other methods I have seen may use a HASH on the argument lengths and then use Call or other currying tricks.
Ric
Steve Brewer (November 14, 2007 at 12:33 pm)
Why not used named params and key route off of them:
function Users(){
addRoutedFunction('find',
{ params : ['name'],
func : function(name){...},
shortName : 'ByName'},
{ params : ['lastName', 'firstName],
func : function(first, last){...},
shortName : 'ByFirstLast'},
function(){/*default function*/});
}
This would give you:
find({firstName : 'steve', lastName : 'brewer'});
findByFirstLast('steve','brewer');
findByName('steve brewer');
find({name : 'steve brewer');
You could also handle situations where different scenarios have the same number of arguments:
findByName, findByCity, findByState,
find({city : '...'}, find({name : '...'}, find({state : '...'}
More like ruby, less like java. That’s a winner in my book.
John Resig (November 14, 2007 at 12:52 pm)
@Ric: Nope, I haven’t seen this particular technique used anywhere else.
@Steve: That’s a significant level of overhead that you have to add in order to handle that situation – as opposed to just using a single entry point for the users to interact with. Granted, the .find() example is a little bit contrived (and in this case, it may be optimal to use an object literal as its argument) but in many cases this technique is optimal for code clarity, if nothing else.
Another example:
// Remove all events
addMethod("removeEvent", function(){});
// Remove all events of a specific type
addMethod("removeEvent", function(type){});
// Remove all event handlers of a type
addMethod("removeEvent", function(type,fn){});
The above is a case where passing in an options hash would not be optimal (both in implementation and in use).
Bertrand Le Roy (November 14, 2007 at 12:55 pm)
That’s a pretty clever hack, no doubt about it, but I’d personally stay clear of it as it makes it even more difficult to tool. If you get a reference to one such overloaded method, you have no practical way of discovering its signature(s). In particular, the length property itself, which is intrumental in this, will always be zero on an overloaded method.
Ric (November 14, 2007 at 1:35 pm)
John,
I am positive I have seen something like this before.
I did a quick check on my bookmarks and found these:
http://www.svendtofte.com/code/curried_javascript/
http://dean.edwards.name/common/help/#common
http://jsfromhell.com/classes/overloader
http://www.fallengods.com/blog/2005/08/07/javascript-function-overloading/
These are not quite what I remember, so I will keep looking. I have also seen a JSON method similar to Steve’s suggestion above which allows me to specify the named arguments, but the script is more complex than yours.
John Resig (November 14, 2007 at 1:50 pm)
@Ric: I don’t doubt that people have done function overloading before – even I’ve written my own overloading routines. However, I like this one because it is fundamentally very simple and terse (as well as, comparatively, quite fast) – which is much more than most other methods.
Michael Geary (November 14, 2007 at 3:24 pm)
That seems like a lot of work to get the compiler to count argument signatures for you.
If you bite the bullet and do a little manual counting, you can make the code simpler and faster:
function Users() {}
Users.prototype.find = function() {
({
0: function() {
// Find all users
},
1: function( name ) {
// Find a user by name
},
2: function( first, last ) {
// Find a user by first and last name
}
})[arguments.length].apply( this, arguments );
};
That’s the whole thing – no addMethod() support function required.
It’s really no great burden to do your own counting like this – you shouldn’t be using more than a few arguments anyway.
If you want a catchall, you can do that too:
function Users() {}
Users.prototype.find = function() {
( ({
0: function() {
// Find all users
},
1: function( name ) {
// Find a user by name
},
2: function( first, last ) {
// Find a user by first and last name
}
})[arguments.length] || function() {
// A different number of arguments given
} ).apply( this, arguments );
};
John Resig (November 14, 2007 at 3:43 pm)
@Michael: I’d argue a couple things:
First, that your technique is considerably slower than the one that I describe (averaging about 40-60ms for 1000 calls to the find method, whereas mine is only about 10-30ms).
Second, that there’s a level of redundancy to your code – having to state the number of arguments that are required for a function (which is annoying if all you want to do is write some elegant code).
Third, having to handle all the details of creating an object literal, accessing one of its properties, and applying against it, does not make for terribly efficient coding. If your concern is over filesize then simply using your technique twice would more than overcompensate for the minimal overhead of the addMethod() function entails.
All of that being said, I do like the idea that you proposed – it’s quite nifty.
Michael Geary (November 14, 2007 at 4:58 pm)
Interesting that the object lookup is so much slower – thanks for running the performance test, John. It goes to show that you can’t assume (as I did!) that one technique is faster than another. To paraphrase Knuth, “I have only proven it faster, I have not tested it.” :-)
Ren (November 14, 2007 at 9:46 pm)
Is there any way to use a function like this to do a multiple replace on an entered string? I’m trying to shorten this script. I always have problems with arrays and think that having to use replace 300,000 times in a script is just inefficient.
function translate()
{
a=document.translationform.translationtext.value ;
a=a.replace(/\s+/g,"");
a=a.replace(/ã„ã¤ã¾ã§ã‚‚(?!’)/g,"itsumademoã„’ã¤â€™ã¾â€™ã§â€™ã‚‚’no matter when#");
a=a.replace(/良ã‹ã£ãŸ(?!’)/g,"yokatta良’ã‹â€™ã£â€™ãŸâ€™was good#");
a=a.replace(/#con/g,"#");
a=a.replace(/’/g,"");
a=a.replace(/#/g,"\n");
document.translationform.translationtext.value=a
}
John Resig (November 15, 2007 at 12:17 am)
@Michael: I bet if you, in your code, broke the object literal outside of the function call it might be faster – although traversing through the closures might slow things back down again (hmmm…). So yeah, not sure what the best strategy is.
@Ren: Don’t think so offhand. However, if you’re wondering about speed you can do:
a = a.split('#').join('\n');
It only works for static strings – but it’s much faster than doing:
a = a.replace(/string/g, 'string');
Sean Kinsey (November 15, 2007 at 2:36 am)
Heres a small function that can be used to create multiple signatures separated by type as well as number of arguments.
//returns an overloadable method
Function.CreateOverloaded=function(){
var overloads=[];
//creates the overloaded method
var func= function(){
var _sign="";
for (var i=0;i<arguments.length;i++){
_sign+=typeof arguments[i]+",";
}
if (_sign.length!==0){
_sign=_sign.substring(0,_sign.length-1);
}
var func=overloads["("+_sign+")"];
if (!func) {throw "No matching signature";}
return func.apply(this,arguments);
}
//method for adding a function with a spesific signature
func.AddSignature=function(func,_sign){
if (overloads["("+_sign+")"]){throw "Duplicate signature"};
overloads["("+_sign+")"]=func;
}
return func;
}
http://kinsey.no/blog/2007/11/15/JavascriptFunctionOverloading.aspx
Ren (November 15, 2007 at 9:54 am)
You mentioned the word ‘split’ and it sent me off looking for other people’s scripts. I finally found something! http://www.thescripts.com/forum/thread157795.html
Thanks.
Michael Geary (November 15, 2007 at 10:49 am)
Ren, you have to use “ampersand lt semicolon” instead of the less-than sign, and “ampersand gt semicolon” instead of the greater-than sign, as shown in the note below the comment box.
John – bingo! It definitely must be the object creation that made my code so slow. Dang, I always forget that. I think to myself, “this will be fast, it’s just a hash table lookup!” But I forget to think, “this will be slow, it’s creating and destroying a temporary object every time it’s called!”
Tom (November 15, 2007 at 10:51 am)
I haven’t dug in, but perhaps an eval of a switch block would outperform either hash or chaining?
Alan Pearce (November 15, 2007 at 10:52 am)
Here is what I came up with. I my tests it seems to run about 30% faster. You could also and an ‘else’ to return a default if the correct method id not found.
function addMethod(object, name, fn){
addMethod.store=addMethod.store||{};
addMethod.store[name+fn.length]=fn;
object[name]=function(){
var funcKey=name+arguments.length;
if(addMethod.store[funcKey])
return addMethod.store[funcKey].apply(this, arguments);
};
}
Scott Olson (November 15, 2007 at 3:58 pm)
Alan (above) and I revamped his code into the method below. By not performing the string concatenation for name and arguments.length, we get an even faster average execution time. _store is now a property of object, not addMethod.
function addMethod(object, name, fn){
object._store = object._store || {};
object._store[name] = object._store[name] || {};
object._store[name][fn.length] = fn;
object[name] = function() {
if(this._store[name][arguments.length])
return this._store[name][arguments.length].apply(this, arguments);
};
}
John Resig (November 15, 2007 at 5:52 pm)
@Alan and Scott: Very nice work. It seems like this would prove to be a nice alternative (and it’s reasonably sized as well, which is good). Personally, adding the extra property would irk me, but depending on the situation I could definitely see using your solution. Great job!
Jonas Raoni (November 17, 2007 at 11:37 am)
I got a referrer in my site log and came here to check the origin haha =)~
My point of view is that emulating such things or proposing “handed-changes” in the structure of language is idiot. Such things should be accomplished by the language itself.
About the overloading, most of the times it’s better to add a simple – if(typeof o == “string”) – instead of creating another function, which will led you to duplicated code or if to organize well, calling chains. Some could also say that you’ll avoid “ifs”, but this advantage will be killed by the hand-made overloading system, so in the end there will be only an “elegant” handling system, which in my opinion is completely useless due to the reason “elegance”.
Also, if to make such things, I prefer the kind of implementation that I and some other people did (with type hinting), which is obviously more natural if comparated with other languages. On JavaScript we don’t have type hinting, so you can say that your one accomplished perfectly the “different method signature”, but if to emulate idiot things, I would go for the also idiot type hinting ^^
BRRRRRR, I shouldn’t comment here haha, it’s against my ideology to take part in blogs xD
Frank Thuerigen (November 18, 2007 at 1:06 pm)
Hi John,
here is my approach for the same thing based on type checking… the “stringstring(…)” seems a bit strange but it is my balance of speed against code beauty. See the code here:
http://blog.phpbuero.de/?p=17
Frank
Trinithis (January 14, 2008 at 7:48 pm)
How about:
Function.prototype.addMethod(name /*, meth1, ... , methN */) = function() {
var map = {};
for(var i = arguments.length - 1; i > 0; --i)
map[arguments[i].length] = arguments[i];
this[name] = function() {
return map[arguments.length];
};
};
Trinithis (January 14, 2008 at 8:05 pm)
Whoops. Forgot the apply. If anything, here’s a version that gives limited support for one variable length function (the one with the highest length prop).
Function.prototype.addMethod(name /*, meth1, ... , methN */) = function() {
var map = {};
var max = 0;
for(var i = arguments.length - 1; i > 0; --i) {
map[arguments[i].length] = arguments[i];
max = Math.max(max, arguments[i].length);
}
this[name] = function() {
return (map[arguments.length] || map[max]).apply(this, arguments);
};
};
reena (February 27, 2008 at 7:31 am)
I sorted a table using onLoad JS as shown in http://www.scriptsearch.com/cgi-bin/jump.cgi?ID=3806.But this script fails to work for nested datatables.Please help
Satish (June 5, 2008 at 6:43 am)
I was looking to implement the function overloading in javascript.. The way you implemented the concept that is owesome. And I love the way you implemented for Object hierarchy…