JavaScript Spread Rest

The three-dot syntax ... in JavaScript can mean two opposite things depending on where it appears: spread, which expands an array (or any iterable) into its individual elements, or rest, which collects multiple individual elements back into a single array. They look identical but do the reverse of one another, and mastering both is essential for writing clean, modern ES6+ JavaScript — especially when working with arrays, function parameters, and destructuring.

Overview: How Spread and Rest Work

Both operators use the exact same three-dot token, but JavaScript tells them apart by context:

  • Spread is used where a list of values is expected — inside an array literal [...], inside a function call’s argument list, or inside an object literal. It takes an existing iterable (array, string, Set, Map, etc.) and "unpacks" it into separate values.
  • Rest is used where individual values are being received — in a function’s parameter list, or on the left side of a destructuring assignment. It gathers "the remaining" values into a brand-new array.

Internally, spread relies on the iterable protocol: when you write [...arr], the JavaScript engine calls arr[Symbol.iterator]() and pulls values out one at a time until the iterator is exhausted, pushing each one into the new array (or argument list, or object). This is why spread works not just on arrays but on strings, Set, Map, NodeList, and any custom object that implements Symbol.iterator.

Rest works differently: it is purely a parameter/destructuring-time construct. When the engine binds arguments to a function, if the last parameter is prefixed with ..., it collects every remaining argument (from that position onward) into a real, brand-new Array instance — unlike the old arguments object, which is array-like but not a true array and does not support methods like .map() or .reduce() directly.

A critical detail beginners often miss: both spread and rest, when applied to arrays of objects, only create a shallow copy. The new array is a new container, but if its elements are objects, those object references are copied — not the objects themselves. Mutating a nested object through either the original or the copy affects both, since they still point to the same object in memory. We’ll see this in action in the Common Mistakes section below.

Syntax

// Spread: expands an iterable into individual elements
const arr1 = [1, 2];
const arr2 = [3, 4];
console.log([...arr1, ...arr2]);

// Rest: collects individual elements into an array
function combine(...values) {
  return values;
}
console.log(combine(1, 2, 3));

Output:

[ 1, 2, 3, 4 ]
[ 1, 2, 3 ]
Form Where used Effect
[...arr] Array literal Expands arr‘s elements into a new array
fn(...arr) Function call Passes each element of arr as a separate argument
{...obj} Object literal Copies obj‘s own enumerable properties into a new object
function fn(...args) Function parameters Collects extra arguments into an array named args
const [a, ...rest] = arr Array destructuring Collects remaining elements into rest

Examples

Example 1: Spreading arrays to combine and copy them

const fruits = ['apple', 'banana'];
const moreFruits = ['cherry', 'date'];
const allFruits = [...fruits, ...moreFruits, 'elderberry'];
console.log(allFruits);

const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original);
console.log(copy);

Output:

[ 'apple', 'banana', 'cherry', 'date', 'elderberry' ]
[ 1, 2, 3 ]
[ 1, 2, 3, 4 ]

The first line shows spread merging two arrays plus an extra literal value into one new array. The second part shows a common and very useful pattern: [...original] creates a new, independent array, so pushing into copy does not affect original.

Example 2: Spreading arrays into function arguments

const numbers = [5, 12, 8, 130, 44];
console.log(Math.max(...numbers));

function volume(length, width, height) {
  return length * width * height;
}
const dimensions = [2, 3, 4];
console.log(volume(...dimensions));

Output:

130
24

Math.max() doesn’t accept an array — it expects separate arguments. Spread solves this instantly by unpacking numbers into Math.max(5, 12, 8, 130, 44). The same trick fills a fixed-arity function like volume from an array of pre-computed values.

Example 3: Rest parameters for variable-length functions

function sum(...nums) {
  return nums.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3));
console.log(sum(10, 20, 30, 40));

function logFirstAndRest(first, ...rest) {
  console.log('First:', first);
  console.log('Rest:', rest);
}
logFirstAndRest('a', 'b', 'c', 'd');

Output:

6
100
First: a
Rest: [ 'b', 'c', 'd' ]

sum accepts any number of arguments because ...nums gathers them all into a real array, which is why .reduce() works directly on it. In logFirstAndRest, only the arguments after the named parameter first get swept into rest — rest always collects whatever is left over.

Example 4: Rest in array destructuring

const scores = [95, 82, 77, 60, 88];
const [highest, ...others] = scores;
console.log(highest);
console.log(others);

const [a, b, ...remaining] = [1, 2, 3, 4, 5];
console.log(a, b, remaining);

Output:

95
[ 82, 77, 60, 88 ]
1 2 [ 3, 4, 5 ]

Destructuring with rest is a clean way to peel off the first item (or first few items) while keeping the remainder as its own array, without manually slicing indices.

Under the Hood

When the engine evaluates [...arr1, ...arr2], it doesn’t treat this as a single bulk-copy operation. It builds the new array element by element: it opens an iterator on arr1, calls .next() repeatedly until done: true, appending each yielded value; then it does the same for arr2. For a plain array this is fast and predictable (equivalent in effect to concat()), but it also means spread works uniformly on any iterable — a Set, a Map‘s .entries(), or even a string ([...'abc'] produces [ 'a', 'b', 'c' ]).

For rest parameters, the engine’s calling convention changes: instead of binding one argument per named parameter, it binds all the leftover arguments (from the rest parameter’s position to the end of the argument list) into one freshly-allocated array object. This happens once, at call time, before the function body runs — so mutating the rest array inside the function has no effect on anything outside it.

Common Mistakes

Mistake 1: Rest parameter not in the last position

A rest parameter must always be the last parameter in a function’s parameter list, because it greedily consumes everything remaining. Writing something like function greet(...names, lastGreeting) {} throws a SyntaxError: Rest parameter must be last formal parameter at parse time — the code won’t even run.

function greet(...names, lastGreeting) {
  console.log(names, lastGreeting);
}

The fix is to put the rest parameter last, moving any fixed parameters before it:

function greet(lastGreeting, ...names) {
  console.log(lastGreeting, names);
}
greet('Hello', 'Alice', 'Bob');

Output:

Hello [ 'Alice', 'Bob' ]

Mistake 2: Assuming spread makes a deep copy

Spread only copies one level deep. If the array holds objects, the new array contains references to the same objects, not fresh clones.

const original = [{ id: 1 }, { id: 2 }];
const shallowCopy = [...original];
shallowCopy[0].id = 99;
console.log(original[0].id);
console.log(shallowCopy[0].id);

const deepCopy = original.map(obj => ({ ...obj }));
deepCopy[1].id = 500;
console.log(original[1].id);
console.log(deepCopy[1].id);

Output:

99
99
2
500

Mutating shallowCopy[0] also changed original[0], because both arrays hold a reference to the exact same object. To truly avoid shared references, spread each individual object too (as deepCopy does), or use structuredClone() for arbitrarily nested data.

Best Practices

  • Use [...arr] instead of arr.slice() to copy an array — it’s more readable and works the same way for merging multiple arrays at once.
  • Prefer rest parameters (...args) over the legacy arguments object — rest parameters are real arrays, work in arrow functions, and are explicit about which parameters exist.
  • Use rest destructuring (const [first, ...rest] = arr) instead of manual index math or .slice(1) when you need to separate the "head" from the "tail" of an array.
  • Remember spread is shallow — when copying arrays of objects that you intend to mutate independently, clone each element too, or use structuredClone().
  • Combine spread with default parameters for flexible APIs, e.g. function log(level = 'info', ...messages) {}.
  • Don’t overuse spread on very large arrays inside hot loops — each spread allocates a brand-new array, which has a real (if usually small) performance cost.

Practice Exercises

  • Write a function average(...nums) that uses rest parameters to accept any number of numeric arguments and returns their average.
  • Given two arrays const a = [1, 2, 3] and const b = [4, 5, 6], use spread to build a single array containing all six numbers, then log it.
  • Given const team = ['Alice', 'Bob', 'Carla', 'Dan'], use array destructuring with rest to extract the captain (first element) and the rest of the team into a separate array, then log both.

Summary

  • Spread (...) expands an iterable into individual elements; rest (...) collects individual elements into a new array.
  • Spread works in array literals, function calls, and object literals; rest works in function parameter lists and destructuring patterns.
  • Spread is context-driven by the iterable protocol, so it works on arrays, strings, Set, Map, and other iterables.
  • Rest parameters produce a real array, unlike the old arguments object.
  • A rest parameter must be the last parameter in a function signature, or JavaScript throws a SyntaxError.
  • Spread only performs a shallow copy — nested objects remain shared references unless you clone them explicitly.