ES6 In Depth: Rest parameters and defaults

ES6 In Depth is a series on new features being added to the JavaScript programming language in the 6th Edition of the ECMAScript standard, ES6 for short.

Today’s post is about two features that make JavaScript’s function syntax more expressive: rest parameters and parameter defaults.

Rest parameters

A common need when creating an API is a variadic function, a function that accepts any number of arguments. For example, the String.prototype.concat method takes any number of string arguments. With rest parameters, ES6 provides a new way to write variadic functions.

To demonstrate, let’s write a simple variadic function containsAll that checks whether a string contains a number of substrings. For example, containsAll("banana", "b", "nan") would return true, and containsAll("banana", "c", "nan") would return false.

Here is the traditional way to implement this function:

function containsAll(haystack) {
  for (var i = 1; i < arguments.length; i++) {
    var needle = arguments[i];
    if (haystack.indexOf(needle) === -1) {
      return false;
    }
  }
  return true;
}

This implementation uses the magical arguments object, an array-like object containing the parameters passed to the function. This code certainly does what we want, but its readibility is not optimal. The function parameter list contains only one parameter haystack, so it’s impossible to tell at a glance that the function actually takes multiple arguments. Additionally, we must be careful to start iterating through arguments at index 1 not 0, since arguments[0] corresponds to the haystack argument. If we ever wanted to add another parameter before or after haystack, we would have to remember to update the for loop. Rest parameters address both of these concerns. Here is a natural ES6 implementation of containsAll using a rest parameter:

function containsAll(haystack, ...needles) {
  for (var needle of needles) {
    if (haystack.indexOf(needle) === -1) {
      return false;
    }
  }
  return true;
}

This version of the function has the same behavior as the first one but contains the special ...needles syntax. Let’s see how calling this function works for the invocation containsAll("banana", "b", "nan"). The argument haystack is filled as usual with the parameter that is passed first, namely "banana". The ellipsis before needles indicates it is a rest parameter. All the other passed parameters are put into an array and assigned to the variable needles. For our example call, needles is set to ["b", "nan"]. Function execution then continues as normal. (Notice we have used the ES6 for-of looping construct.)

Only the last parameter of a function may be marked as a rest parameter. In a call, the parameters before the rest parameter are filled as usual. Any “extra” arguments are put into an array and assigned to the rest parameter. If there are no extra arguments, the rest parameter will simply be an empty array; the rest parameter will never be undefined.

Default parameters

Often, a function doesn’t need to have all its possible parameters passed by callers, and there are sensible defaults that could be used for parameters that are not passed. JavaScript has always had a inflexible form of default parameters; parameters for which no value is passed default to undefined. ES6 introduces a way to specify arbitrary parameter defaults.

Here’s an example. (The backticks signify template strings, which were discussed last week.)

function animalSentence(animals2="tigers", animals3="bears") {
    return `Lions and ${animals2} and ${animals3}! Oh my!`;
}

For each parameter, the part after the = is an expression specifying the default value of the parameter if a caller does not pass it. So, animalSentence() returns "Lions and tigers and bears! Oh my!", animalSentence("elephants") returns "Lions and elephants and bears! Oh my!", and animalSentence("elephants", "whales") returns "Lions and elephants and whales! Oh my!".

The are several subtleties related to default parameters:

  • Unlike Python, default value expressions are evaluated at function call time from left to right. This also means that default expressions can use the values of previously-filled parameters. For example, we could make our animal sentence function more fancy as follows:
    function animalSentenceFancy(animals2="tigers",
        animals3=(animals2 == "bears") ? "sealions" : "bears")
    {
      return `Lions and ${animals2} and ${animals3}! Oh my!`;
    }
    

    Then, animalSentenceFancy("bears") returns "Lions and bears and sealions. Oh my!".

  • Passing undefined is considered to be equivalent to not passing anything at all. Thus, animalSentence(undefined, "unicorns") returns "Lions and tigers and unicorns! Oh my!".
  • A parameter without a default implicitly defaults to undefined, so
    function myFunc(a=42, b) {...}
    

    is allowed and equivalent to

    function myFunc(a=42, b=undefined) {...}
    

Shutting down arguments

We’ve now seen that rest parameters and defaults can replace usage of the arguments object, and removing arguments usually makes the code nicer to read. In addition to harming readibility, the magic of the arguments object notoriously causes headaches for optimizing JavaScript VMs.

It is hoped that rest parameters and defaults can completely supersede arguments. As a first step towards this, functions that use a rest parameter or defaults are forbidden from using the arguments object. Support for arguments won’t be removed soon, if ever, but it’s now preferable to avoid arguments with rest parameters and defaults when possible.

Browser support

Firefox has had support for rest parameters and defaults since version 15.

Unfortunately, no other released browser supports rest parameters or defaults yet. V8 recently added experimental support for rest parameters, and there is an open V8 issue for implementing defaults. JSC also has open issues for rest parameters and defaults.

The Babel and Traceur compilers both support default parameters, so it is possible to start using them today.

Conclusion

Although technically not allowing any new behavior, rest parameters and parameter defaults can make some JavaScript function declarations more expressive and readable. Happy calling!


Note: Thanks to Benjamin Peterson for implementing these features in Firefox, for all his contributions to the project, and of course for this week’s post.

Next week, we’ll introduce another simple, elegant, practical, everyday ES6 feature. It takes the familiar syntax you already use to write arrays and objects, and turns it on its head, producing a new, concise way to take arrays and objects apart. What does that mean? Why would you want to take an object apart? Join us next Thursday to find out, as Mozilla engineer Nick Fitzgerald presents ES6 destructuring in depth.

Jason Orendorff

ES6 In Depth editor

About Benjamin Peterson

More articles by Benjamin Peterson…


9 comments

  1. Oz

    I take it default expressions can be function calls, so

    function setDiscount(product, discount=getDefaultDiscountFor(product))

    would be valid, perhaps even inline functions?

    function setDiscount(saleprice, discount=(function(saleprice) {
    if (saleprice > 100) return 10;
    if (saleprice > 50) return 2.5;
    return 0;
    })(saleprice)) {
    console.log(discount);
    }

    May 22nd, 2015 at 03:57

    1. Benjamin Peterson

      That’s correct. Both of those should work even if the second doesn’t result in the prettiest code. :)

      May 22nd, 2015 at 08:41

  2. Colin P. Hill

    Default values are wonderful, and it’s good to know their semantics, but I would strangle anyone I caught actually putting their logic inside the parameter list.

    May 22nd, 2015 at 18:05

  3. simonleung

    Should an empty array in rest parameter be undefined rather than an array without any element inside? It is not useful usually.

    Besides, we could not use rest parameter and defaults at the same time. i.e.
    function fun(…arr=[1,2]){} will get a syntax error.
    It doesnt make sense?

    May 23rd, 2015 at 06:11

    1. Benjamin Peterson

      It’s thought to be more consistent if the rest parameter is always an array. For example, then you don’t have to check if the parameter is undefined before you try to iterate through it.

      May 23rd, 2015 at 11:41

  4. Ralph

    Are all the arguments assigned before any defaults are applied or is every argument set from left to right.

    example:
    function animalSentence(animals2=animals3,
    animals3=”bears”)
    {
    return `Lions and ${animals2} and ${animals3}! Oh my!`;
    }

    animalSentence(undefined, “turtles”);

    would animals2 be “turtles”, undefined or maybe even “bears”?

    May 26th, 2015 at 07:43

    1. Benjamin Peterson

      The given arguments are assigned to the parameter names and then defaults are evaluated. So, in this example, animals2 would be "turtles".

      May 26th, 2015 at 09:50

  5. Matěj Cepl

    I guess, you meant to say that containsAll should read:

    function containsAll(haystack, …needles) {
    return needles.everry(x => haystack.indexOf(x) !== 0);
    }

    Right?

    May 29th, 2015 at 01:53

  6. Benjamin Peterson

    Yes, every() would work, too, but I wanted to demonstrate for-of loop.

    May 29th, 2015 at 08:11

Comments are closed for this article.