This is a continuation of my previous post A Fine Line Between Abstraction and Obfuscation and, of course, deals with the same material.

JavaScript is flexible in almost every way, and many people end up either abusing the flexibility, or creating strategies of overcoming the flexibility that only create confusion and messy code. I’d like to go over one of my favorite topics today, JavaScript’s function signature, although lack of a function signature is probably a better way to say it. You’ll see that can employ some strategies that can solve these problems without increasing the amount of code you have, while at the same time, providing context for those using your code in the future. Even if you don’t use everything I’m setting out below, I hope it will reveal what to be cautious of, and help you create meaningful solutions of your own.

Dealing with Multiple Function Signatures

Before we begin, I’d like to outline the situation we are trying to avoid by showing you snippets of real source code from an old version of the Dojo Toolkit (there’s no need to analyze all of this, just give it a quick scan):

connect = function(/*...*/){
  if(arguments.length == 1){
    var ao = arguments[0];
  }else{
    var ao = interpolateArgs(arguments, true);
  }
  if(isString(ao.srcFunc) && (ao.srcFunc.toLowerCase() == "onkey")){
    // ...
  }
  if(isArray(ao.srcObj) && ao.srcObj!=""){
    // ...
  }
}

interpolateArgs(args, searchForNames){
  // ...
  switch(args.length){
    case 0: return;
    case 1: return;
    case 2:
      // ..
      break;
    case 3:
      if((isObject(args[0]))&&(isString(args[1]))&&(isString(args[2]))){
        // ...
      }else if((isString(args[1]))&&(isString(args[2]))){
        // ...
      }else if((isObject(args[0]))&&(isString(args[1]))&&(isFunction(args[2]))){
        // ...
      }else if((isFunction(args[0]))&&(isObject(args[1]))&&(isString(args[2]))){
        // ...
      }
      break;
    // We will stop here, you get the idea. 
  }
}

All of this code exists to allow our function (connect) to be called in all sorts of different ways with all sorts of different parameters. This, of course, adds a ton of power to our function, since it fulfills a wide range of uses. But I only ever called this function with a standard set of parameters, so all of this extra power was lost on me.

Allowing the same function to be called with a variety of arguments is built into many modern languages. Most of these make this type of magic pretty fluid and safe. Java forces you to declare them by function signature (basically the quantity and types of the parameters), Python allows you to pass arguments by name and provides errors if done incorrectly, but JavaScript provides no assurance that the parameters passed are the parameters that you wanted.

function clicked(event){
  var id = event.target.id

This is a common event handler, probably something you would write at the very beginning of a project to handle an onclick event. In this example, you are analyzing the node in the DOM that was clicked, and finding the ID that it provided. About a month into the project, though, you realize that you want to be able to call this function using a plain old ID. At first you decide to just to some quick type checking.

function clicked(event){
  if(event.target){
    var id = event.target.id;
  }else{
    var id = event;
  }

Of course, you have to document this. So you probably add some comments to your function signature.

function clicked(/*Event|Integer*/ event){

But has this really improved your code? First of all, your function is still named clicked, even though it probably does an operation that could be described properly in a function name. On top of that, your argument is still named event, even though it could be an event or an ID. My solution would be to move the body of this function into a new function, properly named, and use the clicked function to merely extract the ID and call this new function. You will end up with something like this:

function clicked(event){
  processById(event.target.id);
}

function processById(id){

You just made your code a lot less confusing, you have created two meaningful variable names, and the developer using your code can now effectively ignore the clicked function, since it’s only called by the browser.

This is a simple case, and you will probably start itching at some point to try out your JavaScript voodoo, start doing arguments.length checks, typeof checks, making sure that your arguments are unique and varied enough so that you don’t have any conflicts. Resist this if you can.

I think a good rule of thumb is that each parameter should always do the same thing. That is, you should use what many of us like to refer to as duck-typing to determine your function signature. What this means is that if you have a parameter named url, go ahead and allow it to be a variety of object types, as long as they all indicate URLs. But once you start letting a parameter be a URL or a date, and you figure out which it should be based on variable analysis, you should start worrying. More than anything, the next person to use your function is going to be awfully curious about how they were intended to call it. This is where people argue, “I will just comment it”. But why bother? Instead, it is easy to provide an abstraction to your original function that merely moves your parameters around as appropriate.

When you have created a lot of abstractions of the same function, you might consider one of the heavyweights of the JavaScript function-calling world: the simple object. Instead of passing arguments in a certain order, just have an abstracted function that accepts one parameter with a variety of keywords. Your function can then figure out how to best call your base function.

The problem here is knowing what keywords to use. I prefer creating at least one file in your project to hold dummy objects. To reference the example I laid out at the start of this chapter, your dummy object might look like this:

function dummyArgs(args){
  // id: Integer
  // The ID to process
  this.id = args.id;
}

Of course, you would say which keys are optional, which keys should have other keys, anything you think the person reading your code would find useful. Although be careful if you find yourself writing if…then comments, because it’s a good indication that you have gotten yourself into the parameter soup you were trying to avoid. If one variable truly relies on another, do not force the developer to sift through comments, just throw an error or otherwise get the developer’s attention.

To tell the person using your code to look at this object, add a little comment to your new function:

function processByKwArgs(/*dummyArgs*/ args){

As a footnote, let us look at the issue of optional parameters. Some functions are bound to accept a basic set of parameters, with the possibility of some extras. In this case, your basic set of parameters should be at the start of the function and the optional ones at the end. The position of the optional arguments is usually sorted by the likelihood they will be used, sorted from most to least likely. A good way of denoting this is by adding a question mark to the end of the object type in a comment:

function process(/*Object*/ item, /*Boolean?*/ flag1, /*Boolean?*/ flag2){

Also, you might create a function with a recurring final argument. Even though it is possible, I would discourage against putting a recurring argument anywhere but at the end of the argument. If you want to put it in the middle, simply require that the user pass an array of items. A good way of denoting this is by adding an ellipsis to the end of the object type in a comment:

function processAll(/*Object...*/ item){

If you are using the keyword args approach I just outlined, simply make a note that the key is optional using the same syntax.

Comments Don’t Fix Bad Code

I’ve used a fair amount of comments in the previous section, but I want to make a point of discouraging JavaScript developers from using them to solve just any old problem. When I used them above, I was describing a type, something that is simply not possible in the language.

That said, comments do not fix bad code. Unfortunately, I have seen the problems caused by various abstraction schemes attempted to be solved through commenting. A good rule of thumb is to clean up your code as much as possible and, only then, add comments. Comments should describe what is happening, not what might happen.

My own experience has shown that adding a comment like /*do not use the number 4 here*/ fails miserably. There are lots of posts online about why commenting does not fix bad code, some style guides, and countless veteran programmers that will further confirm this truth, so I will leave it to them.

In Summary

It’s always tempting to be clever for the sake of being clever. And a lot of that attitude is why we find some JavaScript written in our workplace, or even in the open source toolkits, that have functions that can be called a million different ways. It’s one of those things that at first glance seems powerful, or flexible, but it is the truly bad kind of abstraction. Simply noticing when you’re using comments as a crutch, when someone is passing an argument that doesn’t coincide with the named parameter, when you’re calling a function that incorrectly identifies its purpose, or just realizing that properly abstracting what you’ve written doesn’t really add any extra code, will allow you to write truly great code.