Default Parameter Values

Perhaps one of the most common idioms in JavaScript relates to setting a default value for a function parameter. The way we’ve done this for years should look quite familiar:

  1. function foo(x,y) {
  2. x = x || 11;
  3. y = y || 31;
  4. console.log( x + y );
  5. }
  6. foo(); // 42
  7. foo( 5, 6 ); // 11
  8. foo( 5 ); // 36
  9. foo( null, 6 ); // 17

Of course, if you’ve used this pattern before, you know that it’s both helpful and a little bit dangerous, if for example you need to be able to pass in what would otherwise be considered a falsy value for one of the parameters. Consider:

  1. foo( 0, 42 ); // 53 <-- Oops, not 42

Why? Because the 0 is falsy, and so the x || 11 results in 11, not the directly passed in 0.

To fix this gotcha, some people will instead write the check more verbosely like this:

  1. function foo(x,y) {
  2. x = (x !== undefined) ? x : 11;
  3. y = (y !== undefined) ? y : 31;
  4. console.log( x + y );
  5. }
  6. foo( 0, 42 ); // 42
  7. foo( undefined, 6 ); // 17

Of course, that means that any value except undefined can be directly passed in. However, undefined will be assumed to signal, “I didn’t pass this in.” That works great unless you actually need to be able to pass undefined in.

In that case, you could test to see if the argument is actually omitted, by it actually not being present in the arguments array, perhaps like this:

  1. function foo(x,y) {
  2. x = (0 in arguments) ? x : 11;
  3. y = (1 in arguments) ? y : 31;
  4. console.log( x + y );
  5. }
  6. foo( 5 ); // 36
  7. foo( 5, undefined ); // NaN

But how would you omit the first x argument without the ability to pass in any kind of value (not even undefined) that signals “I’m omitting this argument”?

foo(,5) is tempting, but it’s invalid syntax. foo.apply(null,[,5]) seems like it should do the trick, but apply(..)‘s quirks here mean that the arguments are treated as [undefined,5], which of course doesn’t omit.

If you investigate further, you’ll find you can only omit arguments on the end (i.e., righthand side) by simply passing fewer arguments than “expected,” but you cannot omit arguments in the middle or at the beginning of the arguments list. It’s just not possible.

There’s a principle applied to JavaScript’s design here that is important to remember: undefined means missing. That is, there’s no difference between undefined and missing, at least as far as function arguments go.

Note: There are, confusingly, other places in JS where this particular design principle doesn’t apply, such as for arrays with empty slots. See the Types & Grammar title of this series for more information.

With all this in mind, we can now examine a nice helpful syntax added as of ES6 to streamline the assignment of default values to missing arguments:

  1. function foo(x = 11, y = 31) {
  2. console.log( x + y );
  3. }
  4. foo(); // 42
  5. foo( 5, 6 ); // 11
  6. foo( 0, 42 ); // 42
  7. foo( 5 ); // 36
  8. foo( 5, undefined ); // 36 <-- `undefined` is missing
  9. foo( 5, null ); // 5 <-- null coerces to `0`
  10. foo( undefined, 6 ); // 17 <-- `undefined` is missing
  11. foo( null, 6 ); // 6 <-- null coerces to `0`

Notice the results and how they imply both subtle differences and similarities to the earlier approaches.

x = 11 in a function declaration is more like x !== undefined ? x : 11 than the much more common idiom x || 11, so you’ll need to be careful in converting your pre-ES6 code to this ES6 default parameter value syntax.

Note: A rest/gather parameter (see “Spread/Rest”) cannot have a default value. So, while function foo(...vals=[1,2,3]) { might seem an intriguing capability, it’s not valid syntax. You’ll need to continue to apply that sort of logic manually if necessary.

Default Value Expressions

Function default values can be more than just simple values like 31; they can be any valid expression, even a function call:

  1. function bar(val) {
  2. console.log( "bar called!" );
  3. return y + val;
  4. }
  5. function foo(x = y + 3, z = bar( x )) {
  6. console.log( x, z );
  7. }
  8. var y = 5;
  9. foo(); // "bar called"
  10. // 8 13
  11. foo( 10 ); // "bar called"
  12. // 10 15
  13. y = 6;
  14. foo( undefined, 10 ); // 9 10

As you can see, the default value expressions are lazily evaluated, meaning they’re only run if and when they’re needed — that is, when a parameter’s argument is omitted or is undefined.

It’s a subtle detail, but the formal parameters in a function declaration are in their own scope (think of it as a scope bubble wrapped around just the ( .. ) of the function declaration), not in the function body’s scope. That means a reference to an identifier in a default value expression first matches the formal parameters’ scope before looking to an outer scope. See the Scope & Closures title of this series for more information.

Consider:

  1. var w = 1, z = 2;
  2. function foo( x = w + 1, y = x + 1, z = z + 1 ) {
  3. console.log( x, y, z );
  4. }
  5. foo(); // ReferenceError

The w in the w + 1 default value expression looks for w in the formal parameters’ scope, but does not find it, so the outer scope’s w is used. Next, The x in the x + 1 default value expression finds x in the formal parameters’ scope, and luckily x has already been initialized, so the assignment to y works fine.

However, the z in z + 1 finds z as a not-yet-initialized-at-that-moment parameter variable, so it never tries to find the z from the outer scope.

As we mentioned in the “let Declarations” section earlier in this chapter, ES6 has a TDZ, which prevents a variable from being accessed in its uninitialized state. As such, the z + 1 default value expression throws a TDZ ReferenceError error.

Though it’s not necessarily a good idea for code clarity, a default value expression can even be an inline function expression call — commonly referred to as an immediately invoked function expression (IIFE):

  1. function foo( x =
  2. (function(v){ return v + 11; })( 31 )
  3. ) {
  4. console.log( x );
  5. }
  6. foo(); // 42

There will very rarely be any cases where an IIFE (or any other executed inline function expression) will be appropriate for default value expressions. If you find yourself tempted to do this, take a step back and reevaluate!

Warning: If the IIFE had tried to access the x identifier and had not declared its own x, this would also have been a TDZ error, just as discussed before.

The default value expression in the previous snippet is an IIFE in that in the sense that it’s a function that’s executed right inline, via (31). If we had left that part off, the default value assigned to x would have just been a function reference itself, perhaps like a default callback. There will probably be cases where that pattern will be quite useful, such as:

  1. function ajax(url, cb = function(){}) {
  2. // ..
  3. }
  4. ajax( "http://some.url.1" );

In this case, we essentially want to default cb to be a no-op empty function call if not otherwise specified. The function expression is just a function reference, not a function call itself (no invoking () on the end of it), which accomplishes that goal.

Since the early days of JS, there’s been a little-known but useful quirk available to us: Function.prototype is itself an empty no-op function. So, the declaration could have been cb = Function.prototype and saved the inline function expression creation.