Topics in this subject
JavaScript 4 min read Updated 5 Aug 2026

Spread, Rest & Default Params

One syntax (…) two opposite jobs, plus default parameters — the trio everyone confuses, made clear with a mental model, diagram, and gotchas.

Three features people blur together: default parameters fill in missing arguments, and ... does two opposite things depending on where it sits.

The one idea that unlocks it 🎯#

... is the same token with opposite jobs, decided by position:

  • Rest — on the left of = / in a parameter list / in destructuring → it collects many values into one array/object.
  • Spread — on the right / inside a call, array, or object literal → it expands one iterable into many values.

Rule of thumb: rest packs, spread unpacks. If ... is receiving, it's rest. If it's supplying, it's spread.

flowchart LR
    subgraph S["spread … — EXPANDS (one → many)"]
        SA["[1, 2, 3]"] --> s1["1"]
        SA --> s2["2"]
        SA --> s3["3"]
    end
    subgraph R["rest … — COLLECTS (many → one)"]
        r1["1"] --> RA["…args → [1, 2, 3]"]
        r2["2"] --> RA
        r3["3"] --> RA
    end
Rest ... Spread ...
Job collects values into one expands one into values
Lives in param lists, destructuring (the target) calls, array/object literals (the source)
Produces a real array (or object) individual elements
Position must be last anywhere
Example function f(...args) f(...arr)

Default parameters#

A parameter gets a fallback when the argument is undefined (only undefinednot null, 0, or "").

function greet(name = "Guest", greeting = "Hi") {
  return `${greeting}, ${name}`;
}
greet();                 // "Hi, Guest"
greet("Ada");            // "Hi, Ada"
greet(undefined, "Yo");  // "Yo, Guest"  — undefined triggers the default
greet(null);             // "Hi, null"   ⚠️ null does NOT trigger it

Key facts 🎯:

  • Evaluated at call time, left to right, so a later default can use an earlier param: (a, b = a * 2) => {}.
  • Defaults can be any expression (function calls, objects): (id = crypto.randomUUID()) => {}.
  • Combine with destructuring for clean "options objects" — the = {} lets you omit the whole argument:
function connect({ host = "localhost", port = 5432, tls = false } = {}) {
  return `${tls ? "tls" : "tcp"}://${host}:${port}`;
}
connect();                       // uses all defaults (thanks to `= {}`)
connect({ port: 6432 });         // override just one

Rest — collect the leftovers#

// rest parameter — gathers remaining args into a real array
function sum(first, ...rest) {
  return rest.reduce((a, b) => a + b, first);
}
sum(1, 2, 3, 4);                 // 10  (rest = [2, 3, 4])

// rest in destructuring
const [head, ...tail] = [1, 2, 3];       // head = 1, tail = [2, 3]
const { id, ...others } = { id: 1, a: 2, b: 3 };  // others = { a: 2, b: 3 }

⚠️ Rest must be lastfunction f(...args, last) {} is a SyntaxError.

🟢 Prefer ...args over the legacy arguments object: rest is a real array (has .map, .filter), arrow functions have it, and it excludes the params you already named.

Spread — expand into place#

// in function calls
Math.max(...[3, 1, 2]);          // 3
const nums = [1, 2]; sum(...nums, 3);

// in array literals — clone, merge, insert
const clone   = [...arr];                 // shallow copy
const merged  = [...a, ...b];             // concatenate
const inject  = [0, ...mid, 99];          // insert in the middle

// in object literals — clone, merge, override
const patched = { ...user, active: true };   // later keys win 🎯
const withDefaults = { role: "user", ...input }; // input overrides defaults

// spread any iterable
[...new Set([1, 1, 2])];         // [1, 2]  (dedupe)
[..."hi"];                       // ["h", "i"]

⚠️ Spread is a shallow copy — nested objects/arrays are still shared by reference:

const a = { u: { name: "x" } };
const b = { ...a };
b.u.name = "y";
a.u.name;                        // "y"  ⚠️ nested object was shared
// deep copy instead:
const deep = structuredClone(a);

All three together#

function createUser(name = "Anonymous", ...roles) {   // default + rest
  return { name, roles };
}
const base = ["reader"];
createUser("Ada", ...base, "admin");   // spread into the call
// → { name: "Ada", roles: ["reader", "admin"] }

Interview Q&A#

Q1. Same ... — how do you know if it's rest or spread? Position decides: if ... is receiving values (parameter list, destructuring target) it's rest and packs them into one array/object; if it's supplying values (a call, array, or object literal) it's spread and unpacks one iterable into many.

Q2. Does a default parameter trigger for null? For 0? Only for undefined. null, 0, and "" are all passed through as-is — e.g. greet(null) yields "Hi, null", not the default.

Q3. Why prefer a rest param over arguments? ...args is a real array (has .map/.filter/.reduce), it's available inside arrow functions (where arguments isn't), and it excludes the parameters you already named.

Q4. Is object/array spread a deep copy? How do you deep-copy? No — spread is a shallow copy; nested objects/arrays remain shared by reference. Deep-copy with structuredClone(obj) (or a library / JSON.parse(JSON.stringify(...)) with its caveats).

Q5. Why must the rest parameter come last? Rest collects all remaining arguments, so anything after it would be ambiguous — function f(...args, last) {} is a SyntaxError.