Rest and Spread

NRFNRF
6 min read

Among the useful features that have been added to JavaScript is this: ...

That's right, three dots one after the other. These three dots can be used in two ways:

  • spread operator
  • rest parameters

Spread Operator

The spread operator, as the name suggests, "spreads out" the items of an iterable (like an array) into individual elements.

const randomNumbers = [2, 9, 17];

console.log(randomNumbers); // output: [2, 9, 17]
console.log(...randomNumbers); // output: 2 9 17

Notice that, when using the spread operator, the output is no longer an array but rather the individual elements of the array.

There is more to be discussed regarding the spread operator and arrays (and objects). But before that, let me just mention here that the spread operator can also be used on strings as well. In the case of a string, using the spread operator will simply return the individual characters of the string.

const randomString = "defiance";

console.log(...randomString); // output: "d" "e" "f" "i" "a" "n" "c" "e"

Now back to arrays (and objects). First things first, the spread operator can't be used on objects without a surrounding context. For example, the following code will generate an error:

const myObj = { a: "hello", b: "world" };

console.log(...myObj); // this will generate an error

The cool thing about the spread operator is that when it is used within the context of an array/object (meaning used inside square brackets or curly braces), it results in a new array/object containing the elements of the original. Examples are in order.

In the code example below, the spread operator is being used within the context of an array (inside square brackets). First a copy of the array randomNumbers is created using the spread operator and then a new array is created using randomNumbers.

const randomNumbers = [2, 9, 17];
const randomNumbersCopy = [...randomNumbers];
const randomNumbersExtended = [-1, ...randomNumbers];

console.log(randomNumbersCopy); // output: [2, 9, 17]
console.log(randomNumbersExtended); // output: [-1, 2, 9, 17]

The spread operator can also be used to combine multiple arrays to create a new array.

const upperBody = ["head", "shoulders"];
const lowerBody = ["knees", "toes"];
const poem = [...upperBody, ...lowerBody];

console.log(poem); // output: ["head", "shoulder", "knees", "toes"]

The same techniques we just saw in the two examples above can also be used for objects.

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };

const obj1Copy = {...obj1};
const objCombined = {...obj1, ...obj2};
const obj2Extended = {...obj2, e: 5};

console.log(obj1Copy); // output: { a: 1, b: 2}
console.log(objCombined); // output: { a: 1, b: 2, c: 3, d: 4 }
console.log(obj2Extended); // output: { c: 3, d: 4, e: 5 }

Now considering the example above, what would happen if I did something like this const obj3 = { ...obj1, b: 31415 }? The catch here is that obj1 already has a key called b and duplicates are not allowed. In this case, the resulting object will have the updated value of b.

const obj1 = { a: 1, b: 2 };
const obj3 = { ...obj1, b: 31415 };

console.log(obj3); // output: { a: 1, b: 31415 }

This is a nifty trick to have in your arsenal if you ever wish to update just one of the properties of an object while keeping the rest untouched.

Let's do a relatively practical example. Say we have a function that takes exactly three arguments and returns their average. Also, say we have an array of three numbers and wish to know their average.

function average(a, b, c) {
  return (a + b + c) / 3;
}

const randomNumbers = [3, 5, 7];
/**
 * Instead of this:
 * const avg = average(randomNumbers[0], randomNumbers[1], randomNumbers[2]);
 * we can simply use the spread operator here
 **/
const avg = average(...randomNumbers);

console.log(avg); // output: 5

Another practical example would be the Math.max() method. Math.max() returns the largest number in a list of numbers but does not accept an array as its argument. Rather it expects a list of numbers.

const epicListOfNumbers = [1, 2002, 4, 91, 104, 7];

console.log(Math.max(...epicListOfNumbers)); // output: 2002

Rest Parameters

Rest parameters does the exact opposite of the spread operator i.e. it expects a list of numbers and creates an array out of those numbers.

function uselessFunction(a, b, ...rest) {
  console.log(a);
  console.log(b);
  console.log(rest);
}

uselessFunction("first argument", "second argument", "third", "fourth", "and", "sixth argument");

[console output]:

"first argument"
"second argument"
["third", "fourth", "and", "sixth argument"]

So the parameter a of uselessFunction() receives "first argument" and the parameter b receives "second argument"; nothing new here. But, as can be seen in the console output, all the remaining arguments are received by rest as an array. That only happened because of the ... that precedes the parameter rest (the three dots would be called the rest syntax in this case). If the rest syntax had not been there, then the argument "third" would've been received by the parameter rest and the remaining arguments would've simply been ignored.

function uselessFunction(a, b, rest) {
  console.log(a);
  console.log(b);
  console.log(rest);
}

uselessFunction("first argument", "second argument", "third", "fourth", "and", "sixth argument");

[console output]

"first argument"
"second argument"
"third"

In this way, rest parameters allow us to have an indefinite number of arguments. One point to note here is that the rest parameters give us an actual array. So all the Array methods (like map() and reduce()) are available to us.

❗️important

The rest parameters must be the last in the list of parameters. Both of the following function declarations will raise an error:

function restMustBeLast(a, ...rest, b) { // -- } // error

function restMustBeLast(...rest, a, b) { // == } // error

The correct way to declare this function would be:

function restMustBeLast(a, b, ...rest) { // -- }

Let's revisit the average() function we wrote near the end of the spread operator section and improve it. As we programmed it before, average() took exactly three numbers and gave us their average. That is quite limited. Ideally we should be able to give an arbitrary long list of numbers and be able to get their average.

function average(...numbers) {
  const sumOfNumbers = numbers.reduce((accumulator, currentValue) => (accumulator + currentValue));
  const avgOfNumbers = sumOfNumbers / numbers.length;

  return avgOfNumbers;
}

console.log(average(1, 2, 3, 4, 5)); // output: 3
console.log(average(1, 2, 3, 4, 5, 6)); // output: 3.5
console.log(average(1, 2, 3, 4, 5, 6, 7, 8, 9)); // output: 5

💡just a tip

There is an array-like object called arguments which gives us access to all of the arguments passed to a function (except arrow functions which do not have arguments). An indefinite argument list was handled using this arguments variable before the rest parameters were made available in JavaScript. An important point to remember, though, is that arguments is not an actual array, so the Array methods are not available on it.

In this section we looked at the rest syntax in the context of a function's parameter list. In this context, the correct term to use would be rest parameters. The same syntax can also be used in destructuring too as we will see when we discuss destructuring insha'Allah.


👉🏻 Follow me on twitter: click here

👇🏻 Subscribe to my newsletter


0
Subscribe to my newsletter

Read articles from NRF directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

NRF
NRF

I'm a front-end web developer mostly focused on JavaScript. Specifically, React.