Thursday, March 27, 2008

JavaScript Method Overloading

posted by Jonah Dempcy


A feature of languages such as Java, C# and C++ is the ability to overload methods with multiple parameters. Overloaded methods execute different code depending on the type and amount of parameters supplied-- in other words, the type signature of the method. Thus, overloading methods allows for a more flexible API which can handle different possible signatures.

Here's an example of how it works in Java. Let's say that I have a record collection and want to be able to add records by name, ID number, or a Record object:
public void addRecord(String name) {
 // implementation details
}

public void addRecord (Number id) {
 // implementation details
}

public void addRecord(Record record) {
 // implementation details
}
This is convenient because I can call addRecord() elsewhere in the code and use whatever data is available at that time.

Unfortunately, JavaScript does not currently allow you to do this, at least not in this manner. One reason for this is that JavaScript does not have type signatures for its methods. The signatures of JavaScript methods are simply the names of the parameters, without any type information.

Compare:

Java
public void addRecord(String name) {}
JavaScript
function addRecord(name) {}
As you can see, JavaScript method declarations do not have the types of return values or parameters. This makes sense given that JavaScript is not a typesafe language, or a strongly typed language at all.

Regardless, JavaScript 2.0 will support method overloading inherently (so-called 'multifunctions'), but for now, we're left to devise our own solutions to this.

JS Method Overloading: A First Attempt



If your method only has a single argument, it's fairly easy to overload a method.

For instance, here's an overloaded method that hides an element, given either its id (a string) or the HTML object itself.
function hideElement(element) { // takes id of element (string) or HTML obj
  if (typeof element == 'string') {
      element = document.getElementById(element);
  }
  element.style.display = 'none';
}
In this example, a check of the argument's type (using the typeof keyword) determines if it is a string. If so, it redefines element as the HTML object itself, instead of the string id.

Here's how you might use it:
// Create an element and append to the DOM
var myElement = document.createElement('div');
myElement.style.backgroundColor = '#00FF40'; // green
myElement.setAttribute('id', 'greenbox');
document.body.appendChild(myElement);

// Hide the element using a direct object reference
hideElement(myElement);

// Hide the element using the string id
hideElement('greenbox');
This is pretty rudimentary, and certainly doesn't allow for code reuse. Each time you want to overload a method, you have to do all these type checks using the typeof keyword. Not the ideal solution, but workable if you are just overloading a method with one or two type signatures.

JS Method Overloading: Making it reusable

Working from the end back, an ideal solution would be to have a simple utility function to add methods along with their type signature. Something like this:
function addMethod(obj, name, method, signature) {
   // implementation details
}

obj .......... we add the method to this object
name ......... the name of the method
method ....... the method to be added
signature .... the signature of the method, e.g. ["string", "boolean", "number"]
It turns out that someone already came up with this! In November 2007, John Resig wrote a post on JavaScript method overloading that contains a version of this very function. However, although his version is useful and very clever, it does not take into account overloading by type. It only allows overloading based on a different number of arguments.

Let's try implementing the addMethod() function now. Warning, this attempt fails (so don't copy from the code below, because it won't work) -- but it's educational to see the process of developing the code, so I've left this example in:
function addMethod(obj, name, method, signature) {

    // we will use this variable to check if a method already exists 
    // with the same name

   var existingMethod; 
   
   if (typeof obj[name] == 'function') { // if method already exists
        existingMethod = obj[name];      // store a reference to it
    }

    obj[name] = function(arguments) {        
        var signatureMatches = true;
        for (var i = 0; i < signature.length; i++) {
            if (signature[i] != typeof arguments[i]) signatureMatches = false;
        }

        if (signatureMatches) method((arguments);

        if (existingMethod) existingMethod(arguments);
    }
}
At first glance, this looks like it might work, but alas, does not. It somehow overwrites the existing methods so it is not working correctly. But fret not, for there is a working solution. As they say, when you fail, try, try again.

Finally, a Working Solution

This solution may not be the most elegant, but it gets the job done. This is licensed under the MIT license so feel free to use it for your projects (per the terms of the license).
function addMethod(obj, name, method, signature) {
    // The first time addMethod() is called, define the method
    if (typeof obj[name] == 'undefined') {
        obj[name] = function() {
            for (var signatureString in obj.overloadedMethods[name].signatures) {
                var signatureArray = signatureString.split(",");
                
                var signatureMatches = true;                
                
                for (var i = 0; i < arguments.length; i++) {
                    if (typeof arguments[i] != typeof signatureArray[i]) signatureMatches = false;
                }            
                
                if (signatureMatches) {
                    obj.overloadedMethods[name].signatures[signatureString](arguments);
                }

            }
        }
    }

    // The first time addMethod() is called, create overloadedMethods property
    if (typeof obj.overloadedMethods == 'undefined') {
        obj.overloadedMethods = {};
    }
    
    // The first time a method is added for a given name, create it
    if (typeof obj.overloadedMethods[name] == 'undefined') {
        obj.overloadedMethods[name] = {'signatures': {}};
    }
    
    // Store the method in an associative array (hash map)
    // keyed by the signature string, e.g. "number,boolean,array"
    obj.overloadedMethods[name].signatures[signature.toString()] = method;
}
This approach differs from the previous one in that I am saving an associative array of methods' signatures mapped to the method itself. Here are some tests which demonstrate how to use it:
// tests
var jonah = {};
addMethod(jonah, 'test', function(str) {alert("it's a string");}, ['string']);
addMethod(jonah, 'test', function(num) {alert("it's a number");}, ['number']);

jonah.test('string'); // alerts "its a string"
jonah.test(123); // alerts "it's a number"
The addMethod() function is actually only creating one method for "test", a method which iterates through the stored methods in the overloadedMethods object, checking the signature of each one. When it finds a match, it calls that method. Simple enough, I think.

Here's another example:
addMethod(jonah, 'test', function(bool, str) {alert("it's a boolean, and string");}, ['boolean', 'string']);

jonah.test(true); // this should do nothing
jonah.test(true, "this should work");
There is definitely room for improvement to this code. For one, you can break out of the outer for() loop once finding a match to save some cycles.

Another optimization is perhaps rewriting the way it finds the match. But I'll leave these for the topic of another article. Comments are welcome. Feel free to post your own improvements to the code, or alternate solutions.

Labels:

0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]

<< Home