Understanding JavaScript spread and rest syntax
11 Dec 2021
In this guide we'll be looking at two closely-related features of modern Javascript, namely spread syntax and rest parameters.
Both use the syntax ...
followed immediately by an iterable/object (spread) or a function argument (rest) but they're used in different ways.
Spread syntax 🔗
Spread syntax is a way to "expand" the contents of an iterable such as an array or string into its constituent parts. It can also be used to spread objects into key-value pairs in certain situations.
For example, suppose the following function:
function greet(title, name) {
alert(`Hi, ${title} ${name}!`);
}
let person = ['Mr', 'Johnson'];
greet(...person); //"Hi, Mr Johnson!"
In the above example, our person data (title and name) exists in the form of an array. We could instead have passed our function arguments like so:
...but spread syntax means we can just pass the expanded array instead to achieve the same result.
This illustrates spread syntax nicely, but it's a limited example. The true power of spread syntax is to work with any number of items, whereas our function expects only two.
This means that, while we can use spread to pass a dynamic number of function arguments, our function isn't dynamic enough yet to accept any number of arguments. Let's rectify that by meeting rest parameters.
Rest parameters 🔗
Rest parameters look just like spread syntax but they're used in function definitions to capture the remaining arguments into an array (i.e. after any other, preceding arguments have captured individual arguments).
In the following example, a
will have the value "foo" and b
, due to its rest syntax, will capture the remaining values passed into an array, ["bar1", "bar2"]
.
Often rest params are used with numbers. In the following example, we'll pass a sequence of numbers - and the sequence can be of any length - and we'll get back the same sequence as an array but with each number incremented by one.
Note that a function can have only one rest parameter in its signature, and it must be the last one in the list. So all of the following will yield parse errors:
function foo(a, ...b, c) { //isn't last argument
function bar(...a, ...b) { //more than one rest param
Also note that rest parameters capture only remaining values, i.e. not those already captured by preceding arguments in the list, if any.
Before rest parameters came along, to implement our plusOne()
function we'd have had to do one of three things.
First, we could rigidly code our function to expect a finite number of arguments.
That's no good; our function is no longer dynamic and is useless any time we want to pass more than four numbers.
Better (but still less ideal) would be to either:
- Derive the passed arguments from the array-like
arguments
object. - Pass the numbers to the function as an array, rather than individual arguments
The arguments approach is the traditional, pre-rest parameters workaround:
That's pretty neat, and works fine. But it's important to note a few differences between rest parameters and arguments
:
arguments
is an array-like object, not a true array. This means we can't use array methods on it, likemap()
, which is why above we had to first derive a true array from it viaArray.from()
arguments
has its own idiosyncrasies not common to proper arrays, like itscallee
property- As noted above, rest parameters scoop up only the extra parameters into an array - i.e. not those already captured by preceding arguments, whereas
arguments
contains all arguments passed to the function
Combining the two 🔗
Earlier, we showed how, via spread syntax, we could pass a dynamic number of arguments to a function. However, our function wasn't ready to receive a dynamic number of arguments.
We now know how to remedy that, via rest parameters. Let's use our plusOne()
function to combine spread syntax and rest parameters to create a truly dynamic function:
//no change here
function plusOne(...nums) {
return nums.map(num => num+1);
}
//but let's add in spread for the invocation
let nums = [1, 3, 7];
if (true) nums.push(9, 11); //some condition
plusOne(...nums); //[2, 4, 8, 10, 12]
Now both our function and its invocation are truly dynamic and will work for any number of arguments.
A replacement for apply() 🔗
One of the most obvious uses for spread syntax is as a replacement for apply()
.
Function.prototype.apply()
, if you don't know, is a way of passing an array to a function that is expecting separate arguments (i.e. not an array). (It also allows you to stipulate the context in which the function runs, but that's off-topic here.)
Where previously we might have done this:
We can now simply do:
A happy consequence of this is you can use constructors, i.e. with the new
keyword, which isn't possible with apply()
. This limitation makes sense; if you do this:
...JavaScript will think you're trying to instantiate apply
(the last method in the chain), not bar
. Spread syntax has no such problem:
Spread syntax - supercharged array literals 🔗
Spead syntax gets even cooler when you consider just how much imperative code it saves you writing. To this end it can be used in a number of common array-handling situations.
For example,suppose you wanted to merge two arrays. Let's look at the pre- and post-spread syntax approaches:
let a = [1, 2],
b = [3, 4],
c;
//before spread syntax
c = [].concat(a, b);
//shiny modern JavaScript
c = [...a, ...b];
Or what if you wanted to "unshift" some new values unto the start of an array?
One subtle difference: Array.prototype.unshift()
modifies the source array in-situ, whereas the spread syntax approach merely overwrites it with a new, revised array
Spread syntax and object literals 🔗
ECMA 9 (2018) saw object literals invited to the spread syntax party - in certain situations. Basically, they can be spread within an object context.
So this will fail:
...because an object isn't an iterable, and you can't expand a non-iterable into an array (which is an iterable).
Likewise, you can't use spread an object when passing it to a function, for the same reason.
But you can do some cool things like clone or merge objects:
This is the equivalent of (indeed, a replacement for) the more traditional Object.assign()
approach.
Both approaches merge only own (not inherited), enumerable properties. One difference, though, is that Object.assign()
will trigger any setter methods present on the object, while spread syntax will not, because the latter results in a new object being created, not a base object being modified.
Summary 🔗
So there you have it. Spread syntax and rest parameters look the same in that they both centre around ...
. They're often used in conjunction with one another.
Spread syntax is used to "expand" iterables or objects, while rest parameters are used to "capture" or group remaining arguments passed to a function that haven't already been captured by preceding arguments in the list.
Spread syntax in particular offers shorter syntax for many common operations such as merging arrays and objects or pushing new items onto the start of an array, while rest parameters allow functions to accept any number of arguments.
Did I help you? Feel free to be amazing and buy me a coffee on Ko-fi!