Spread and Rest Operators
Both use the same ... syntax but do opposite things depending on context: spread expands a collection into individual elements; rest collects individual elements into a collection.
🎯 What you'll learn
- Spread in Array Literals
- Spread in Object Literals
- Spread in Function Calls
- Rest in Function Parameters
- Rest in Destructuring
Both use the same ... syntax but do opposite things depending on context: spread expands a collection into individual elements; rest collects individual elements into a collection.
1Spread in Array Literals
const a = [1, 2];
const b = [3, 4];
const combined = [...a, ...b]; // [1, 2, 3, 4]
const withExtra = [0, ...a, 2.5]; // [0, 1, 2, 2.5]
const copy = [...a]; // shallow copy, not the same array reference2Spread in Object Literals
const base = { theme: "light", retries: 3 };
const merged = { ...base, retries: 5 }; // { theme: "light", retries: 5 } — later keys win
const copy = { ...base }; // shallow copyBoth array and object spread are shallow: nested objects/arrays inside are still shared by reference (identical caveat to Object.assign, covered in the Objects topic).
3Spread in Function Calls
function sum3(a, b, c) { return a + b + c; }
const nums = [1, 2, 3];
sum3(...nums); // 6 — equivalent to sum3(1, 2, 3)
Math.max(...[4, 8, 1]); // 8 — Math.max doesn't accept an array directly, spread fixes that4Rest in Function Parameters
function sum(...numbers) { // collects ALL remaining arguments into a real array
return numbers.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10
function logFirst(first, ...rest) { // rest collects everything AFTER named params
console.log(first, rest);
}
logFirst(1, 2, 3); // 1 [2, 3]Rest parameters must be the last parameter — function f(...rest, last) {} is a syntax error.
5Rest in Destructuring
const [head, ...tail] = [1, 2, 3, 4]; // head=1, tail=[2,3,4]
const { id, ...otherFields } = { id: 1, name: "Ana", age: 30 };
// id=1, otherFields={ name: "Ana", age: 30 }Same last-position rule applies here too.
6Telling Spread and Rest Apart
| Context | ... means |
|---|---|
Inside [...] or {...} literal, or as a function call argument | spread — expand |
| As a function parameter, or on the left side of destructuring | rest — collect |
function example(...args) { // rest: args becomes an array
console.log(Math.max(...args)); // spread: expand args back out for Math.max
}Under the HoodTwo Genuinely Different Mechanisms Behind One Symbol▾
Array spread and object spread look identical at the syntax level, but they are implemented by completely different internal operations — this is worth knowing precisely, not just as trivia, because it explains what each one can and can't do.
Array spread runs the iterator protocol, for real
const arr = [...someIterable];Spreading inside [...] (or as function-call arguments) literally invokes someIterable[Symbol.iterator]() and calls .next() repeatedly until done: true — the exact mechanism from the Iterators and Generators topic. This is why spread works on any iterable (strings, Map, Set, generators, custom iterable classes), and why spreading a non-iterable value ([...{ a: 1 }], a plain object) throws TypeError: is not iterable — there's no Symbol.iterator method for the engine to call.
function* naturalNumbers() { let n = 1; while (n <= 3) yield n++; }
console.log([...naturalNumbers()]); // [1, 2, 3] — spread drives the generator via real .next() callsObject spread does NOT use iteration at all
const merged = { ...base, ...overrides };{...obj} is specified as an internal CopyDataProperties operation: it directly enumerates obj's own enumerable properties (the same set Object.keys would return) and copies each one — no Symbol.iterator involved whatsoever. This is why object spread works on plain objects (which aren't iterable) but a Map spread with {...someMap} does not copy its entries the way [...someMap] would — {...} only ever sees a Map instance's own enumerable properties, which is essentially none of its actual entries (they're stored internally, not as regular properties — see the Map topic).
Rest parameters allocate a real new array, every call
function sum(...numbers) { /* ... */ }Every single call to sum allocates a fresh array object to hold the collected arguments — this isn't free, though it's cheap in absolute terms and, crucially, it's a real array (unlike the legacy arguments object from the Functions topic), so it doesn't carry that object's historical optimization penalties.
Why spreading enormous arrays into a call can fail
fn(...hugeArray); // hugeArray with, say, 200,000 elementsFunction calls have an engine-enforced maximum argument count (well into the tens of thousands, but finite) — spread turns each array element into a genuine, individual function argument at the call site, and exceeding that limit throws RangeError: Maximum call stack size exceeded or a similar limit error, because arguments occupy real stack space per the Call Stack topic. Chunking the array or using .apply() with careful batching avoids this; regular iteration (for, .reduce()) never hits the limit because it never turns array elements into call arguments at all.
⚠️ Common Pitfalls
- Assuming spread deep-copies — it's shallow, exactly like
Object.assign; nested structures remain shared references. - Spreading a
Set/Map/string when an array is expected elsewhere — spread works on any iterable, not just arrays:[..."abc"]gives["a", "b", "c"]. - Using rest anywhere but the last position — always a syntax error, whether in function parameters or destructuring patterns.
- Spreading huge arrays into function calls —
fn(...hugeArray)can hit engine argument-count limits (tens of thousands of elements); use.apply()or chunking for very large collections. - Object spread key order — later spread sources overwrite earlier ones for duplicate keys, left to right; getting the order backwards silently produces the wrong merge result.
📋 Quick Reference
[...arr] → spread: expand array into elements
{...obj} → spread: expand object into key-values
fn(...args) → spread: expand array into arguments
function f(...a) → rest: collect arguments into array
const [x, ...y] = arr → rest: collect remaining into array
const {x, ...y} = obj → rest: collect remaining into object🎤 Interview Q&A
Q1Spread vs rest: what's the difference?core▾
Same ... syntax, opposite roles: spread expands an iterable/object into elements/properties ([...a, ...b], {...obj, x: 1}, f(...args)); rest collects remaining items into an array/object (function f(...args), const [h, ...t] = arr).
- Where can rest appear? Only last in parameter lists or destructuring patterns.
Q2How do you merge objects and what wins?core▾
{...defaults, ...options}: later properties override earlier ones. It's shallow: nested objects are replaced, not merged.
- Deep merge? Use a library or structuredClone plus custom merge, beware prototype pollution.
Q3Is spread a deep copy?core▾
No, it copies one level; nested objects/arrays are shared references.
- Deep copy option? structuredClone.
Q4What can be spread into arrays vs objects?senior▾
Arrays/arguments accept any iterable (strings, Sets, Maps); object spread copies own enumerable properties from any object (spreading null/undefined is ignored).
- Spread a string into an array?
[...'hi']gives ['h','i'] (handles Unicode code points).
Q5What are performance considerations of spread?core▾
Spreading large arrays or objects in loops (e.g. in reduce: {...acc, [k]: v}) creates O(n²) copying. Mutate a local accumulator or use Map when building large structures.
- When is immutability worth it? State management where change detection relies on new references.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.