Rest parameters and spread syntax
Plenty of built-in functions happily take as many arguments as you throw at them. You don’t declare a fixed number of slots and hope the caller matches; the function adapts to whatever arrives.
A few you’ve probably already met:
Math.max(arg1, arg2, ..., argN)— hands back the largest of the numbers you pass.Object.assign(dest, src1, ..., srcN)— copies properties from every source object intodest.Array.prototype.push(item1, ..., itemN)— appends any count of items to an array.
This lesson is about writing your own functions that behave this way, and the mirror-image problem: taking an array you already have and feeding its elements in as separate arguments. Two features do the work, and they share the same three-dot spelling: rest parameters and spread syntax.
Passing too many arguments is fine
Start with a surprise. A function defined with two parameters can still be called with five, and JavaScript won’t complain:
function sum(a, b) {
return a + b;
}
alert( sum(1, 2, 3, 4, 5) ); // 3
No error for the “extra” arguments. But sum only named a and b, so it only uses the first two: 1 + 2 is 3. Everything after 2 is silently ignored by this function body.
So the language lets you pass more, it just doesn’t give you a name to reach the leftovers. That’s the gap rest parameters fill.
Rest parameters ...
Put three dots before a parameter name in the definition and that name becomes an array holding every remaining argument. Read the dots as an instruction: “gather what’s left into an array.”
function sumAll(...args) { // args is an ordinary array
let sum = 0;
for (let arg of args) sum += arg;
return sum;
}
alert( sumAll(1) ); // 1
alert( sumAll(1, 2) ); // 3
alert( sumAll(1, 2, 3) ); // 6
Now nothing gets dropped. Whatever count of arguments comes in, args grows to fit, and a plain for..of walks the whole thing.
Try it live. Type any list of numbers and watch ...args gather all of them into one array, however many you supply:
You don’t have to sweep up everything. Name the first few parameters normally and let a trailing rest parameter collect the tail:
function showName(firstName, lastName, ...roles) {
alert( firstName + ' ' + lastName ); // Maya Vance
// the leftovers land in roles
// roles = ["Curator", "Speaker"]
alert( roles[0] ); // Curator
alert( roles[1] ); // Speaker
alert( roles.length ); // 2
}
showName("Maya", "Vance", "Curator", "Speaker");
Here firstName grabs "Maya", lastName grabs "Vance", and everything past that point flows into roles. Because roles is a genuine array, roles.length, roles.map(...), and every other array method are available.
The arguments object
Before rest parameters existed, functions could still reach their arguments through a special value named arguments. Every regular function gets one automatically — an array-like object indexed from zero that holds all the values it was called with.
function showName() {
alert( arguments.length );
alert( arguments[0] );
alert( arguments[1] );
// it's iterable, so this works too:
// for (let arg of arguments) alert(arg);
}
// shows: 2, Maya, Vance
showName("Maya", "Vance");
// shows: 1, Arjun, undefined (no second argument was passed)
showName("Arjun");
You’ll run into arguments in older codebases, and it still works today. But two things make rest parameters the better default.
First, arguments is array-like, not an array. It has a length and numeric indexes and you can iterate it, but it lacks the array methods. arguments.map(...) throws, because there’s no map on it. A rest parameter is a real array, so nothing is off-limits.
Second, arguments is all-or-nothing: it always mirrors the complete argument list. There’s no way to say “skip the first two and give me the rest,” which is exactly the partial capture that showName(firstName, lastName, ...roles) made trivial.
Spread syntax
Rest parameters go from a list of arguments into an array. Spread syntax runs the other direction: it takes an iterable and unpacks its elements into a comma-separated list, right where you write it.
The motivating case is Math.max, which wants numbers as separate arguments:
alert( Math.max(3, 5, 1) ); // 5
Now suppose those numbers live in an array. Handing the array over directly fails, because Math.max sees one argument (an array) rather than three numbers, and can’t compare it:
let arr = [3, 5, 1];
alert( Math.max(arr) ); // NaN
Typing them out by hand — Math.max(arr[0], arr[1], arr[2]) — only works if you know the length ahead of time. In real code the array might hold ten items or none, and hard-coded indexes fall apart.
Spread solves it. Write ...arr inside the call and JavaScript expands the array into individual arguments:
let arr = [3, 5, 1];
alert( Math.max(...arr) ); // 5
Spread scales up. Pass several iterables in one call:
let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];
alert( Math.max(...arr1, ...arr2) ); // 8
And mix spreads with plain values, in any order:
let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];
alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25
The engine reads left to right, dropping each element into place, so 25 still wins.
Building arrays with spread
Spread isn’t limited to function calls. Inside an array literal it splices one array’s elements into another:
let arr = [3, 5, 1];
let arr2 = [8, 9, 15];
let merged = [0, ...arr, 2, ...arr2];
alert(merged); // 0,3,5,1,2,8,9,15
Reading the literal in order: 0, then everything from arr, then 2, then everything from arr2. This is the clean way to concatenate arrays without concat.
Edit either list below and rebuild the merged array. Notice the elements are spliced in place — you get one flat array, never an array nested inside another:
Any iterable, not only arrays
Spread works on any iterable, and strings are iterable — they yield characters. So spreading a string into an array literal splits it into individual letters:
let str = "Hello";
alert( [...str] ); // H,e,l,l,o
Under the hood, spread uses the same iteration machinery as for..of. A for..of over "Hello" visits "H", "e", "l", "l", "o" one at a time, and [...str] collects exactly those into a new array.
Spread vs. Array.from
Array.from also turns something into an array, and for the string above it gives the same result:
let str = "Hello";
// Array.from converts an iterable into an array
alert( Array.from(str) ); // H,e,l,l,o
Same output, but the two tools cover different inputs:
Array.fromaccepts array-likes (objects with alengthand numeric indexes) and iterables.- Spread accepts iterables only.
So Array.from is the more general converter. Given something like the arguments object in a modern context, or a DOM NodeList that happens to be array-like but not iterable, Array.from handles it while spread would throw.
Copying arrays and objects
Spread gives you a compact way to make a shallow copy. You may recall doing this with Object.assign in Object copying — spread reaches the same result with less ceremony.
Spread an array into a fresh literal and you get a new array with the same elements, but a separate identity:
let arr = [1, 2, 3];
let arrCopy = [...arr]; // spread the elements into a brand-new array
// same contents?
alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true
// same array, though?
alert(arr === arrCopy); // false — different reference
// changing the original leaves the copy alone:
arr.push(4);
alert(arr); // 1,2,3,4
alert(arrCopy); // 1,2,3
The same trick works for objects, spreading into an object literal instead:
let obj = { a: 1, b: 2, c: 3 };
let objCopy = { ...obj }; // copy every own enumerable property into a new object
// same contents?
alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true
// same object?
alert(obj === objCopy); // false — different reference
// changing the original leaves the copy alone:
obj.d = 4;
alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4}
alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3}
This reads shorter than Object.assign({}, obj) for objects or Object.assign([], arr) for arrays, so it’s the go-to when a plain copy is all you need.
See the shared reference bite. Make a spread copy, then rewrite only the clone’s pet — the original changes too, because both point at one nested pet object:
Summary
When you spot ... in code, it’s one of two features, and position tells them apart:
- At the end of a function’s parameter list, it’s a rest parameter. It gathers the remaining arguments into a real array.
- In a function call, array literal, or object literal, it’s spread syntax. It expands an iterable into a list of separate items.
Where each earns its place:
- Rest parameters let you write functions that accept any number of arguments and treat them as an array.
- Spread syntax feeds an existing array (or any iterable) into a function that expects a flat list, merges arrays and objects, and makes quick shallow copies.
Between them, moving from a list to an array and back again stops being a chore. And for legacy code, remember the array-like arguments object still holds every argument of a regular function call — though a rest parameter is the better choice in new code.