Post Archive

› January 11, 2006

JavaScript Function Arguments: Assigning Defaults

  • Reported by Dave

This article demonstrates a nice technique for setting default values for function arguments in JavaScript. The basic form of the idiom is:

function foo(a, b, c) {
    var b = (b == null) ? "default" : b;
    var c = (c == null) ? 100 : c;
    ...
}

Within the body of a function, the argument b will receive the value of "default" if no corresponding value is passed to the function. This statement is repeated for each argument requiring a default value.

Not being one to leave well enough alone, I wanted to "improve" on this idea by creating a function to simplify the assigning of these defaults. Is this an improvement? You be the judge.

First, here is the function:

1.  function defaults(passed) {
2.      var pattern = /function[^(]*\(([^)]*)\)/;
3.      var args = passed.callee.toString().match(pattern)[1].split(/\s*,\s*/);
4.      var str = "", i = 1;
5.      for ( ; i < arguments.length; i++) {
6.          if (typeof passed[i-1] == "undefined") {
7.              str += args[i-1] + "=" + fix(arguments[i]) + ";";
8.          }
9.      }
10.     return str;
11.
12.     function fix(x) {
13.         if (typeof x == "string")
14.             return "'" + x.replace(/\'/g, "\\'") + "'";
15.         return x;
16.     }
17. }

And here is how to use it:

function foo(a, b, c) {
    eval(defaults(arguments, 1, 2, 3));

    alert(a); // defaults to 1
    alert(b); // defaults to 2
    alert(c); // defaults to 3
}

The defaults function requires the first argument to be the arguments object of the enclosing function (in this case, foo). The rest of the arguments are the default values for the corresponding arguments passed to foo.

The defaults function bears some explaining:

  • The calling function (foo) is turned into a string, and its argument list is extracted (lines 2,3)
  • The arguments passed to foo are examined (line 5). These are contained in the first argument passed to defaults, which is the arguments object of foo (confusing isn't it?)
  • If any of these arguments are null, meaning no value was passed, the default value is assigned (lines 6,7)
  • Assigning a variable at this point would make it local to the defaults function. We want it to be in the scope of foo. In order to do this, we need to return the assignment as a string then eval it (cue groaning from certain JavaScripters). For instance, if foo is called with no arguments, defaults will return: "a=1;b=2;c=3;". If it is called with one argument, defaults will return "b=2;c=3;", and so on
  • The call to eval in foo effectively assigns the arguments with their default values
  • The nested function fix ensures that strings are handled properly. Since defaults returns a string to be evaled, string assignments need to be wrapped in quotes and have any nested quotes escaped.

Some shortcomings, real or perceived:

  • Only primitive types (numbers, booleans) and strings are handled
  • Uses eval which some people dislike
  • Might not be that much of an improvement over the "canonical" way of assigning defaults

It might not lend itself to some people's coding style or philosophy, but if you find yourself setting default values often in your functions, this technique offers a clean, consistent way of doing it.

Comments

1. January 11, 2006 03:49 PM

Quote this comment

Menno Posted…

It looks like this would result in global variables being declared. Since function parameters are always local, you might want to change line 7 to the following:

str += "var " + args[i-1] + "=" + fix(arguments[i]) + ";";

2. January 11, 2006 05:00 PM

Quote this comment

Patrick Fitzgerald Posted…

I'm not sure if I'm missing something, but I find it easier to use an object to pass arguments, which has the added advantage of making the arguments optional and position-independent.

myfunction({ c:5, a:22  });
function myfunction(passedArgsObj) {

  /* defaults */
  args = { a:1, b:2, c:3 };

  /* override the defaults if necessary */
  for (var argName in passedArgsObj) {
    args[argName] = passedArgsObj[argName];
  }

  alert("a = "+args.a);
  alert("b = "+args.b);
  alert("c = "+args.c);
}

3. January 12, 2006 04:12 AM

Quote this comment

Vincenzo Posted…

If a parameter is not passed, it has a default value of undefined, not null. So you should check for the undefined value.

As someone already mentioned in Dan's blog, javascript performs implicit conversion when the == operator is used.

0 == null
false == null
undefined == null

This is why, the check for null is true if the parameter is not passed. But it is wrong, because if you pass an explicit value of 0, it will also evaluates to true.

You have to use, one of these comparisons:

a) passed[i-1] === undefined
b) typeof passed[i-1] == "undefined"

4. January 12, 2006 10:20 AM

Quote this comment

Dave Posted…

Thanks Vincenzo for catching that. That is correct. I have updated the function.

5. January 15, 2006 05:02 AM

Quote this comment

lon Posted…

My approach would be to enhance the original function by re-creating it holding the required default code.

So: serialize it, enhance it, re-create it

That way you don't need the evil eval, variables are in the correct scope and there is no extra function call.

6. February 8, 2006 02:06 AM

Quote this comment

patrick Posted…

Why not using :

function foo(a, b, c) {
var b = b || "default";
var c = c || 100;
...
}

7. February 23, 2006 03:54 PM

Quote this comment

Runsun Pan Posted…

The form:

var b = b || 'default'

is dangerous. Anything that is evaluated "false" will be converted to 'default':

function f(a,b){
a = a || "aaa"
b = b || true
return [a,b]
}

alert(f()) ==== > got expected ['aaa', true]
alert(f(4,5)) ====> got expected [4,5]
alert(f(0,false)) ===> expected [0, false], but got ['aaa',true]

8. June 14, 2006 09:55 AM

Quote this comment

Matthias Miller Posted…

Why not use a variation of Patrick Fitzgerald's solution?

Function.prototype.defaults = function(origArgs/*, defaults*/) {
    for (var i = 0, j = 1; j < arguments.length; i++, j++) {
        if (typeof origArgs[i] == 'undefined')
            origArgs[i] = arguments[j];
    }
};
function foo(a, b, c) {
    this.defaults(arguments, 1, 2, 3);

    alert(a); // defaults to 1
    alert(b); // defaults to 2
    alert(c); // defaults to 3
}