Spread Operator — Shallow Merge That Killed User Settings
Shallow spread from `{.
- Spread unpacks iterables into individual values; rest collects remaining values into a real array.
- Spread creates shallow copies — nested objects are shared references, not clones.
- Rest must be the last parameter; it's the only way to get a true Array from variadic arguments.
- Performance cost: spreading large arrays (10k+ items) blocks the event loop ~0.5ms per 10k.
- Production trap: merging nested configs with spread silently overrides deeper keys instead of merging them.
- Biggest mistake: assuming spread deep-clones — it doesn't, causing mutation bugs in state management.
Imagine you ordered a pizza and want to share each slice individually — you 'spread' the pizza across plates. Now imagine a waiter collecting all leftover slices into one box — that's 'rest'. The three dots (...) in JavaScript do exactly this: spread unpacks a collection into individual pieces, and rest gathers individual pieces back into a collection. Same symbol, opposite jobs — context decides which one you're using.
Every modern JavaScript codebase is full of three little dots — ... — and for good reason. Whether you're merging objects in a Redux reducer, passing dynamic arguments to a function, or cloning arrays without mutating the original, the spread and rest operators are doing the heavy lifting. They're not just syntactic sugar; they fundamentally change how you think about data flow in JavaScript.
Before ES6 introduced these operators, copying arrays meant using .slice(), merging objects required Object.assign(), and handling variable-length function arguments meant wrestling with the awkward arguments object. That code was verbose, error-prone, and frankly hard to read at a glance. The ... syntax replaced all of that with something readable enough that your intent is obvious the moment someone looks at your code.
By the end of this article you'll know the difference between spread and rest at a conceptual level (not just syntactically), you'll recognise the real-world patterns where each one shines, you'll understand the gotchas that trip up even experienced developers, and you'll be ready to answer the interview questions that separate candidates who 'know the syntax' from those who actually understand JavaScript.
Spread Operator: Unpacking Collections Where You Need Individual Items
The spread operator takes an iterable — an array, a string, a Set, or any object with a Symbol.iterator — and expands it in-place. Think of it as opening a bag and tipping everything out onto the table.
The most important word there is 'in-place'. Spread doesn't return anything on its own; it works by expanding values into the surrounding context. That context might be an array literal, a function call, or an object literal — and each context behaves slightly differently.
When you spread inside an array literal [...a, ...b], you're building a new array from the elements of both. This is a shallow copy — primitives are copied by value, but nested objects are still referenced. When you spread inside a function call Math.max(...scores), you're passing each element as a separate argument. When you spread inside an object literal { ...defaults, ...overrides }, later keys win, which makes it perfect for configuration merging.
The key insight: spread is about placement. You're deciding exactly where the unpacked values land, and that precision is what makes it so powerful for immutable update patterns.
cartCopy[0].name = 'Trackpad' would also change originalCart[0].name. Use structuredClone() for deep copies.Rest Parameters: Capturing 'Everything Else' Into One Clean Collection
Rest is the mirror image of spread. Where spread expands, rest collects. Its job is to gather up a variable number of values and bundle them into a real JavaScript array that you can then work with normally.
The critical word is 'real array'. Before rest parameters existed, functions used the arguments object to access extra arguments. arguments looks like an array but isn't — it has no .map(), no .filter(), no .reduce(). Developers constantly had to convert it. Rest parameters made that hack obsolete.
Rest must always be the last parameter in a function signature. Importantly, rest only collects arguments that don't have an explicit parameter waiting for them. Named parameters come first, then rest catches whatever remains.
const { password, token, ...safeUser } = userData) is one of the cleanest ways to remove sensitive fields before passing data down to a UI or returning it from an API.The Senior Level: Forwarding and Higher-Order Patterns
In production, rest and spread are often used together to create 'Transparent Wrappers'. This pattern allows you to intercept a function call, add logic (like logging or timing), and then forward the exact original arguments to the underlying function without knowing what those arguments are.
this binding — arrow functions capture this lexically, so if you use rest in an arrow wrapper, you may lose the expected context.this binding and hot-path performance.Performance and Memory: When Spread Costs You
Spread is elegant, but it's not free. Every ...array call allocates a new array and copies all elements. For small arrays it's negligible, but if you're spreading a 100k-item log array in a hot loop, you'll see GC pressure and jank.
Object spread is even more subtle: it calls Object.assign() under the hood and enumerates all own properties. If the object has getters or a heavy prototype chain, those getters execute during spread — leading to side effects you didn't expect.
V8 optimises spreads for simple cases, but the allocation still happens. For immutable updates in React, the framework relies on reference identity, so spreading every render can create new objects that trigger unnecessary re-renders if not memoised.
unshift reduced pauses to 15ms.useSelector memoisation and flooding the UI with re-renders.useMemo or manual reference equality.Edge Cases: Sparse Arrays, Strings, and Iterables
Spread works on any iterable, but iterables aren't always arrays. Strings are iterable — [...'hello'] gives ['h','e','l','l','o']. Great for splitting. But emoji? [...'👍'] returns ['👍'] correctly because strings are Unicode-aware. That's better than .split('') which can break multi-byte characters.
Sparse arrays are tricky. Array(5).fill() creates a dense array, but Array(5) alone leaves holes. Spreading a sparse array preserves the holes as undefined? Actually, [...Array(5)] returns [undefined, undefined, undefined, undefined, undefined] — the holes become undefined. That's a change! If you need to preserve sparseness, use Array.from().
Another gotcha: arguments is not an iterable in strict mode? Actually arguments is iterable in ES2015+, but still not an array. Spread works: [...arguments]. But better to just use rest parameters.
- Spread loops over the iterable using its
[Symbol.iterator]method. - Each iteration yields a value; spread places it into the new array/object.
- If the object is not iterable, you get a TypeError.
- Strings, Set, Map, NodeList are iterable. Plain objects are not (use
Object.keys()first). - Generator functions return an iterable, so
[...someGenerator()]works.
[1, , 3]). Spreading that array produced [1, undefined, 3] which passed validation incorrectly and caused downstream crashes.Array.from for sparse preservation.The Config Merge That Silently Killed User Settings
{ ...defaults, ...userOverrides } would deep-merge the nested objects, preserving all nested keys.defaults and userOverrides are replaced entirely, not merged. The displayPreferences object in userOverrides clobbered the corresponding object in defaults, dropping any keys not explicitly set by the user.structuredClone on defaults and then merging overrides with a custom deep assign). For React state, ensure each nested level is spread independently.- Spread only copies one level deep — always deep-merge nested configs for user preferences.
- Add a unit test that verifies nested object keys survive a merge.
- Use "shallow" as a mental tag every time you write
{...a, ...b}.
JSON.parse(JSON.stringify(obj)) or structuredClone() for full isolation.empty items (e.g., from Array(5)). Use Array.from() instead of spread to convert sparse arrays.Object.assign() with a target that has the correct prototype, or use a factory method.{...defaults, ...userOverrides} with a deep merge function. For quick patch: JSON.parse(JSON.stringify(defaults)) then spread overrides.Key takeaways
..., opposite roles.map(), .filter(), and .reduce().Common mistakes to avoid
5 patternsSpreading an object into an array
[...obj] throws a TypeError because plain objects are not iterable by default.Object.keys(), Object.values() or Object.entries() first to convert the object into an iterable.Expecting spread to deep-clone
structuredClone(obj) in modern environments. For older platforms, JSON.parse(JSON.stringify(obj)) or a utility like Lodash cloneDeep.Placing rest parameter before others
SyntaxError: Rest parameter must be last formal parameter at script parse time.function process(first, ...rest) — never before a named parameter.Using spread on arguments inside an arrow function
arguments is not available in arrow functions. [...arguments] throws a ReferenceError.const myArrow = (...args) => args.Assuming object spread merges arrays
{ ...a, ...b } where both a and b have an array property — the result contains only the array from b, lost all items from a.{ ...a, items: [...(a.items || []), ...(b.items || [])] }.Interview Questions on This Topic
Explain why `const arr2 = [...arr1]` is safer than `const arr2 = arr1` in a React application.
arr2 = arr1 creates a reference copy; mutating arr2 also mutates arr1, which can cause side effects in React's reconciliation. Spread creates a new array reference, ensuring the original array is not mutated. This is crucial for immutability in state management.Frequently Asked Questions
That's Advanced JS. Mark it forged?
3 min read · try the examples if you haven't