Introduction
One of the strong points of JavaScript is that it is weakly typed, it allows a lot of flexibility, unfortunately this means that function overloading isn't available. We don't have explicit parameter type declarations, thus we cannot "type" parameters when we declare functions.
But how much do we really need function overloading? We can do without it, we can simply have the function check for the passed parameter types and lengths of the arguments and based on those, decide on the logic. This is actually how many developers choose to do it, however in my opinion this makes the code less readable/maintainable and it also adds custom code to each function that needs overloading logic and this introduces a layer of coding that may introduce bugs. So let's try to see how we can best achieve this in JavaScript.
I have looked around the net and there are few approaches to achieve this. However, they are not as flexible and as fast to implement as I want. If I want to go from point A -> point B in my code (point B having function overloading feature), I'd like to do that with minimal coding. I'd like to be able to add this to my already existing code in as short a time as possible and I usually like the conventions over configurations approach in such matters.
In this article, I will ask you to do one function call on your JavaScript class and with that call, you will automatically have function overloading. There are still some conventions you need to follow, but they are simple and straight forward, there is almost little or no configuration needed to get this to work.
What Do We Need?
First, we need a way to write our code normally without overloading, but it should be written with few conventions in mind to allow the overloading to be added easily. And second, we need a simple way to "tell" type information from the parameters that we define.
For step one, I have chosen a simple method of writing different functions that will be overloaded into one function. Here, I want to number my functions. Say you have 3 different functions named add
that needs to be overloaded, then the way you write your prototype functions is by adding a suffix to each function call, this suffix is a simple counter. An example explains this best. I want to have 3 different flavors of a function called add
, then what I need to do is create three functions named add1
, add2
and add3
. Later on, my code will go and look for these "numbered functions" and combine them into one overloaded function whose name is without any number so: add1
, add2
, add3
... addX
-> add
(overloaded to serve all those functions).
For step two, I need an easy, fast to implement and simple to remember method of telling each parameter its type. Here convention over configuration plays very well, I have chosen to name my parameters using the good old Hungarian notation method. So my parameters will start with one or more letters that will describe the type and then stop at the first capital letter, that's where the name of the parameter starts. Few examples: nNumericParam
, strName
, oSomeObject
and so on... In the previous examples, these parameters have types "n
", "str
" and "o
". We will map these to actual types with a very simple object that we will pass to the function that creates the overloads for us.
So let's recap and give a small complete example of how we need to write a class.
function AdditionClass() {
this.IsAdditionClass = true;
}
AdditionClass.prototype = {
add1: function(nValue1, nValue2) {
if (console)
console.log('calling add1 with params:', arguments);
return nValue1 + nValue2;
},
add2: function(nValue, strValue) {
if (console)
console.log('calling add2 with params:', arguments);
return nValue + strValue;
},
add3: function(nValue1, nValue2, nValue3) {
if (console)
console.log('calling add3 with params:', arguments);
return nValue1 + nValue2 + nValue3;
}
}
In this class, we wrote these functions add1
, add2
and add3
that will be overloaded to a function called add
(Note that you should not create or use the function name add
in your class, or the function overloader will fail). We also named our parameters using the Hungarian notation which will allow us to "tell" its types.
Using the Code
To add function overloading, we need to call just one function. overloadPrototype
.
This function takes two parameters, the first parameter being the constructor of the class and the second parameter being an object that maps Hungarian notation prefixes to actual types that you can get from typeof
. You may need custom type matching logic in your own code, so for your convenience the actual matching happens in a function called checkTypesMatch
which gets passed the parameter itself and the Hungarian prefix of the parameter it should belong to. I would like to note here that parameter checking is not always employed, if the different functions to be overloaded have unique arguments lengths, then the overloading code will decide solely on arguments lengths, this will be explained in detail later in this article.
In our previous example, the call to add overloading looks like this:
overloadFunction(AdditionClass, { n : "number", str : "string", o : "object" });
Under the Hood
So what does the function overloadFunction
really do. First, it loops over the functions of the prototype of the constructor passed in, once it finds a function with the last character being "1
", it passes on this function name to a function named processFunctions
that will try to hook up the overloads.
The function processFunctions
starts with getting the base name of the function name passed in. In this example, passing in the function name "add1
" means that the base name is add
and the new overloaded function will be created with this name.
Second, processFunctions
will loop and try to find all the counted functions, in this example add1
, add2
and add3
. It will stop when it can't find any more functions or when it has counted a maximum of 10 functions (10 overloads). (Note that when maintaining your code, you cannot leave gaps in the numbered functions). I assume that you should not have more than 10 overloads as this might make things slow for the overloaded functionality, we will go over performance later in this article. If you have more than 10 overloads and you really need them, you may want to increase the upper end of the loop.
Third, processFunctions
will sort this list of functions based on their arguments length, in the process, it will check and see if we have duplicate argument lengths or not. Here we have two cases, duplicate lengths or not and this is indicated with the variable duplicateLen
.
Let's take the easy case first, when we don't have duplicate lengths. This case is easier to code the overloaded function and the overload logic is also very fast, it's cost is basically just another level of indirection to the function call. processFunctions
now will hand over the constructor, the array of the counted functions and the base name to another function called createLengthOverload
.
The function createLengthOverload
creates a new function with the base name (here being "add
") and this function is very simple, it will look up the passed arguments length and based on that will call the appropriate original function. (here being one of add1
, add2
and add3
).
In our second case, where we have functions having a common length, obviously we need to decide which function to call based on type information, this is where things become ugly, well just a little.
In this case, first we need to create a new data structure (called data
in the code) that consolidates the lengths, this means we group the functions having the same lengths. The length X is used as a key and the value is an Array of function information.
Now we need to go over this new data structure (data
) and check which lengths have more than one function, in our example above, add1
and add2
both have arguments lengths 2 and add3
has arguments length 3. So add3
is safe, it doesn't have other functions that compete on its arguments lengths, however add1
and add2
do compete, so the way to resolve which one to call will be based on type matching decision(s). For each group of functions that share a common length, we now create a new data structure for them called annotationArray
, this structure will "describe" for each function the types of the parameters it has (here the two functions add1
and add2
). The annotationArray
will contain for add1
: ['number', 'number'] and for <code>add2
: ['number', 'string']. At this point, we do some sanity checks, we need to make sure these different functions have at least one different parameter type, or else we can't decide which one to call, at least one parameter should be different for each different function (in our example, the second parameter is number for add1
and string for add2
). Type information for common parameters are deleted next (with optimization flag set), in our case, we delete the type information for the first parameter(s) for add1
and add2
, as they are both numbers, they don't serve any purpose in the decision making, we just keep the second parameter information. Now we are ready to create our overloaded function, we pass on the data structure along with the base name and constructor to function createTypeOverload
.
The function createTypeOverload
will add a new function with the base name (here being "add
") similar to createLengthOverload
but the logic will be different. The newly created add
function will have access to the (data
) data structure that we have created and polished earlier. Based on it and on the arguments lengths passed to it, it will decide which function to call (add1
, add2
or add3
). If it is passed 3 parameters, it will directly call add3
, if passed two, it will go through the candidate functions, in this case being add1
and add2
and will try to eliminate the ones that don't fit based on the data types, eventually just one function should win the match.
Flags
optimizeTypeInfo
: This should be set to true
, basically this will remove matching information for overloads if they have the same type for all parameters across all same length candidates. This will speed up the matching process however this means that if all the candidates have the same parameter (say number) and you pass in a string, it will be matched to the first candidate even though it is not number. An example describes this best, add1
and add2
have both numeric types as their first argument. If this flag is set to true
, then the first parameter is never matched, so even if you pass a string
to the first parameter, it will be accepted and will not be used in the matching process. If this flag is set to false
, then all parameters types must be matched exactly. If you pass in a string
to the first argument, it won't match neither add1
or add2
and it will fail with an exception.
matchNull
: Another limitation is passing null
as a parameter value. Since we cannot infer type from null
, we have to decide whether to match anything or match nothing. If you choose to match nothing, this effectively means you cannot pass null
at all to any of your overloaded functions as it will not match. If you set it to match null
(true
), this means null
values will match anything (like a wildcard). We may create a simple convention here to pass in null
and still describe its type, possibly using an object that can easily be tested in the checkTypesMatch
function. But this is left up to the reader to see if this is needed or not.
Performance
Obviously things don't happen magically and there is a penalty to pay, the question is how much is this penalty and whether or not you are ready to pay it. In other languages, the compiler does the overloading so they come free of charge, in JavaScript we have to do what the compiler does roughly but at runtime. If you have critical code, optimizing it without function overloading would be the way to go of course, however if your code is not critical, let's see how much is our penalty. In the case where the arguments lengths are unique across all the functions to be overloaded, this case is fast, very fast, almost without a penalty I would say. If the argument's length are less than 10, I use an array with the argument length as an index to the function to call, so the penalty here is just a layer of indirection that comes from calling apply
on the function itself. If the argument's length are above 10, then I use a hash to get to the function call, which would be slightly more expensive. When we have common lengths, we have to go over the different candidates and eliminate them one by one, so depending on the number of functions that share the same arguments lengths, the penalty will be different. If I have time, I will do some stress testing and post some numbers for some common cases.
Notes About the Code Attached
The code attached is written to run in NodeJS, hence use the require
method to include files. For client side, please include the files using standard script tags.
Contact
I tried different cases with different lengths and/or types to make sure the code works without any problems. If in case you find something or have a trouble using it, please send me a message to my CodeProject account and I will try to fix it ASAP.
Thank you.