JAVASCRIPT / FUNCTIONS
Spread syntax for calls and literals
Use ... to expand arrays, strings, Sets, and objects into call arguments, array elements, and object properties, and know where the copy stops.
What you will learn
- Replace fn.apply(null, arr) with fn(...arr), and use spread with new as well.
- Build arrays as [...a, item, ...b] instead of concat plus push.
- Merge with { ...defaults, ...overrides } knowing the later spread wins.
- Explain why [...plainObject] throws while { ...plainObject } succeeds.
Understanding Spread syntax for calls and literals
Spread is rest read backwards: rest collects loose values into one name where a function is defined, while spread takes one value apart where a function is called or a literal is built. When the engine reaches ...expr inside an argument list or an array literal, it runs that value's iterator and drops each produced value into the surrounding list at exactly that position. The expansion finishes before the function body starts, so the callee receives ordinary separate arguments and has no way to detect that a spread was used.
The same three dots mean two different things depending on where they appear. In a call or an array literal, spread demands an iterable: it looks up Symbol.iterator, which is why arrays, strings, Sets, Maps, and NodeLists work while a plain object throws a TypeError. In an object literal, spread ignores iteration completely and copies own enumerable properties instead, which is why { ...obj } works, and why { ...[7, 8] } produces the index keys 0 and 1 rather than a two-element list.
Spread copies values, and it stops after one level. A nested array or object is copied as a reference, so the copy and the original keep sharing it, which is why { ...state } feels safe right up to the moment you mutate state.list through the copy. It also flattens what it touches: getters run once and their results become plain data properties, non-enumerable and inherited properties are skipped, and the prototype is not carried over, so spreading a class instance leaves you with a plain object that has lost its methods.
function volume(length, width, height) {
return length * width * height;
}
const dims = [2, 3, 4];
console.log(volume(...dims));
const padded = [0, ...dims, 10];
console.log(JSON.stringify(padded));
const base = { unit: "cm", scale: 1 };
const scaled = { ...base, scale: 2 };
console.log(JSON.stringify(scaled));
console.log(base.scale);Three dots at a use site expand one value into individual arguments, elements, or properties at that exact position, one level deep.
Worked examples
Spread at a call site
Shows spread doing the job of Function.prototype.apply, including cases apply cannot handle.
const temps = [18, 25, 21, 30, 27];
console.log(Math.max(...temps));
console.log(Math.max.apply(null, temps));
console.log(Math.max(100, ...temps));
console.log(Math.max(...[]));
const parts = [2024, 0, 15];
console.log(new Date(...parts).getFullYear());Example explained
Line 1Math.max(...temps) is expanded at the call site, so Math.max receives five separate numbers rather than one array.
Line 2apply does the same expansion but forces a this value (null here) that Math.max never uses.
Line 3Spreading an empty array passes zero arguments, and Math.max with no arguments returns -Infinity, its identity value.
Line 4new Date(...parts) is something apply cannot express, because apply calls a function and cannot construct one.
Two different spread rules
Demonstrates that call and array spread need an iterable while object spread only needs own enumerable keys.
const tags = new Set(["a", "b", "a"]);
console.log([...tags].join("-"));
console.log([..."hey"].length);
const config = { host: "localhost", port: 8080 };
try {
[...config];
} catch (err) {
console.log(err.constructor.name);
}
console.log(JSON.stringify({ ...config }));
console.log(JSON.stringify({ ...[7, 8] }));Example explained
Line 1A Set has a Symbol.iterator, so spreading it yields insertion order with the duplicate already dropped.
Line 2Strings are iterable, so spreading 'hey' produces three single-character elements.
Line 3Array spread asks the value for an iterator; a plain object has none, so a TypeError is thrown before anything is copied.
Line 4Object spread uses the own-enumerable-property rule, so config copies fine and an array contributes its index keys 0 and 1.
Merge order decides the winner
Shows that spreads apply left to right and that an explicit undefined still overwrites.
const defaults = { retries: 3, timeout: 1000 };
const options = { timeout: 250, verbose: true };
console.log(JSON.stringify({ ...defaults, ...options }));
console.log(JSON.stringify({ ...options, ...defaults }));
const wiped = { ...defaults, timeout: undefined };
console.log("timeout" in wiped, wiped.timeout);Example explained
Line 1Spreads run in source order like a series of assignments, so options.timeout of 250 overwrites the default 1000.
Line 2Reversing the two spreads makes defaults win, which is nearly always a bug in an options merge.
Line 3Key order follows first insertion, so timeout keeps its early slot even when a later spread changes its value.
Line 4A key written as undefined is still written, so it overwrites the default instead of falling back to it.
The copy is one level deep
Proves that a nested array is shared between the spread copy and the original.
const original = { name: "kit", tags: ["red", "blue"] };
const copy = { ...original };
copy.name = "kat";
copy.tags.push("green");
console.log(original.name, copy.name);
console.log(original.tags.join(","));
console.log(original.tags === copy.tags);Example explained
Line 1Assigning copy.name writes into the new object only, so the top level really is independent.
Line 2tags was copied as a reference, so pushing through copy is visible through original.
Line 3The === check confirms there is one array and not two, which is exactly what one-level copying means.
Important notes
Object spread tolerates null and undefined, so { ...null } is simply {}, while call and array spread throw a TypeError on them.
Spreading a class instance or a Date drops the prototype and internal state: { ...new Date() } is {} and instance methods disappear.
Common mistakes
Spreading a plain object into a call or array literal, as in sum(...options) or [...options], which throws 'options is not iterable' and aborts the call; pass Object.values(options) when you want the values.
Treating { ...state } as a deep copy and then pushing into copy.items: the original array mutates too, and code that compares references sees no change at all.
Calling Math.max(...bigArray) on a few hundred thousand elements, which throws RangeError: Maximum call stack size exceeded because every element becomes a real argument.
Try it yourself
Change, predict, then run
In a browser console, define const scores = [72, 95, 88] and const player = { level: 1, scores }, then build const next = { ...player, level: 2, scores: [...scores, 100] } and log next.scores === scores alongside the same check for a plain { ...player }, and account for the difference.
Open the JavaScript workspaceCheck your understanding
For some value v, [...v] throws a TypeError but { ...v } returns { x: 1 }. What does that tell you about v?
- v is a plain object with own enumerable properties but no Symbol.iterator method
- v is frozen, so array spread is not allowed to read it
- v is an array whose elements happen to be non-enumerable
- v is null or undefined, which only object spread tolerates
Show answer
Array and call spread look up Symbol.iterator and throw when it is missing, while object spread ignores iteration and copies own enumerable keys, so a plain object fails the first and passes the second. Null or undefined is tempting because object spread does accept them, but then { ...v } would be {} rather than { x: 1 }, and freezing affects writes, not reads.