JavaScript’s spread (…) and rest (…) look like “copy” tools, but they perform shallow operations whose safety depends on context: arrays vs objects vs calls, the data’s depth, and whether you mean clone, merge, omit, transform, or expand arguments. This guide is a compact diagnostic: when spread/rest are appropriate, where they fail, and which alternatives to use.
Further reference:
- MDN Spread syntax: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
- MDN structuredClone: https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone
Pick your goal: what does “safe copy” mean here?
| Goal | Tool | Works for | Limitations |
|---|---|---|---|
| New top-level container | […arr], {…obj} | Shallow copy | Nested objects/arrays still shared |
| Merge plain objects | {…a, …b} | Shallow merge (right-most wins) | Nested branches replaced, not deep-merged |
| Gather args / omit fields | function(…args) / { a, …rest } | New array/object created | Still shallow for nested values |
| Deep independent copy | structuredClone(value) | Deep clone for many built-in types; supports cycles | Doesn’t clone functions or preserve custom prototypes |
Shallow copy: when spread/rest is the right, simple choice
If your data is flat (primitives or values you won’t mutate), spread is concise and correct.
Array: new container, same elements
const a = [1, 2, 3];
const copy = [...a];
copy.push(4);
console.log(a); // [1, 2, 3]
console.log(copy); // [1, 2, 3, 4]If elements are objects, only references are copied:
const a = [{ id: 1 }, { id: 2 }];
const b = [...a]; // new array, same object refs
b[0].id = 99;
console.log(a[0].id); // 99 — shared objectObject: top-level props copied
const user = { name: 'Ari', age: 20 };
const userCopy = { ...user };
userCopy.age = 21;
console.log(user.age); // 20But nested properties remain shared:
const user = { name: 'Ari', contact: { email: 'a@example.com' } };
const userCopy = { ...user };
userCopy.contact.email = 'b@example.com';
console.log(user.contact.email); // 'b@example.com' — sharedTargeted immutable updates: copy only the levels you change
When you will mutate nested data, clone each level you will touch rather than deep-cloning everything. This keeps updates predictable and performant.
const state = {
user: { name: 'Ari', contact: { email: 'a@example.com' } },
tags: ['js', 'web'],
};
const next = {
...state, // top-level copy
user: {
...state.user, // copy user
contact: {
...state.user.contact, // copy contact
email: 'b@example.com', // change
},
},
tags: [...state.tags, 'accessibility'], // copy array + append
};
console.log(state.user.contact.email); // 'a@example.com'
console.log(next.user.contact.email); // 'b@example.com'For arrays of objects, create new element objects only if you will change them:
const products = [{ id:1, price:10 }, { id:2, price:20 }];
const updated = products.map(p => p.id === 2 ? { ...p, price: 25 } : p);
// updated[1] is a new object; products[1] unchangedIf your code involves async updates, coordinate changes to avoid races — see JavaScript Promises & Async/Await Explained.
Merging settings and descriptor caveats
Object spread is a shallow merge; the right-most source wins for colliding keys.
const defaults = { theme: { dark:false, contrast:'normal' }, pageSize: 10 };
const user = { theme: { dark:true } };
const merged = { ...defaults, ...user };
console.log(merged.pageSize); // 10
console.log(merged.theme); // { dark: true } — contrast lostTo merge nested branches explicitly:
const deepMerged = {
...defaults,
...user,
theme: { ...defaults.theme, ...user.theme },
};Object spread reads enumerable own properties (invoking getters) and writes plain data properties: accessors and prototypes are not preserved. For class/prototype semantics, see JavaScript Classes Explained.
Rest and spread in calls: gather vs expand
Rest (…) gathers values into a new array/object in parameter lists or destructuring; spread expands iterables into literals or argument lists. Neither deep-clones nested values.
function sum(...nums) { return nums.reduce((s,n) => s + n, 0); }
sum(1,2,3); // 6
const user = { id: 1, name: 'Ari', password: 'secret' };
const { password, ...publicUser } = user; // publicUser is a shallow copy
Spread in calls expands an iterable into positional arguments (it does not clone):
const nums = [3,7,2];
const max = Math.max(...nums); // 7When you need an independent deep copy
Use a deep-clone tool when shallow copies are insufficient. In modern environments, structuredClone is the recommended built-in.
const original = {
user: { name: 'Ari' },
tags: ['js','web'],
created: new Date(),
map: new Map([['k','v']]),
};
const clone = structuredClone(original);
clone.user.name = 'Bea';
clone.tags.push('ui');
console.log(original.user.name); // 'Ari'
console.log(original.tags.length); // 2structuredClone supports many built-ins and circular references, but it does not clone functions and it won’t resurrect custom prototypes. Check MDN for environment support: structuredClone documentation.
JSON.parse(JSON.stringify(…)) is an older shortcut that only works for JSON-compatible, acyclic data and will drop or transform functions, undefined, Dates, NaN, Infinity, and symbols.
If you handle API payloads, confirm the shape before cloning/mutating — see Introduction to APIs for Beginners.
Arrays of objects: decide whether elements must be new too
const a = [{ id:1, meta:{active:true} }, { id:2, meta:{active:false} }];
const b = [...a, { id:3 }]; // new array, same element objects
// To isolate element changes:
const c = a.map(item => ({ ...item })); // shallow element copies (meta still shared)
const d = a.map(item => structuredClone(item)); // deep element copies (independent)Prefer the least cloning required for correctness to avoid unnecessary work.
Prototype, symbols, and descriptor loss
- Prototype: spread produces a plain object with own enumerable properties; methods and prototype chain are not copied.
- Accessors: getters run during copy; resulting property is a plain value, not an accessor.
- Enumerability: non-enumerable properties are skipped; enumerable symbol keys are copied.
class Person { constructor(name){ this.name = name } greet(){ return `Hi, ${this.name}` } }
const p = new Person('Ari');
const data = { ...p }; // { name: 'Ari' } — greet() goneDiagnostic checklist
- Define intent:
- Top-level container only? Or independent nested values?
- Merge vs clone vs argument expansion?
- Check depth: are there nested structures you will mutate?
- Simulate the mutation on the copy: if the original changes too, you share references.
- Confirm semantics: merges are shallow; getters become values; prototypes aren’t preserved.
- Choose the tool:
- Use spread/rest for shallow copies, merges, and gathering/expanding values.
- Use structuredClone for deep cloning of supported types.
- Use JSON methods only for JSON-shaped, acyclic data.
Concrete gotchas
Omitting a field doesn’t scrub nested secrets
const user = { id:1, name:'Ari', tokens:{ access:'abc', refresh:'xyz' } };
const { tokens, ...safeUser } = user; // safeUser.tokens is gone, but tokens object still exists
// To truly sanitize:
const sanitized = { ...safeUser, tokens: { access: '[redacted]', refresh: '[redacted]' } };Merging replaces nested branches
const a = { cfg:{ retries:3, mode:'fast' } };
const b = { cfg:{ retries:5 } };
const merged = { ...a, ...b }; // merged.cfg is { retries:5 } — mode lostGetter becomes a plain value
const metrics = { get now(){ return Date.now(); } };
const snapshot = { ...metrics };
console.log(typeof snapshot.now); // 'number' — no getter preservedAlternatives to copying everything
- Immutable updates: copy only changed branches (as shown above).
- Normalize data: store lists of IDs and a map of objects by ID so updates touch fewer nodes.
- Derive views: use map/filter to create derived arrays rather than mutating originals.
For common pitfalls and state organization patterns, see Common JavaScript Mistakes Beginners Make and JavaScript Classes Explained.
Three Questions to Ask Before Copying
Before writing [...value] or {...value}, identify what must be independent. Is only the outer container changing, or will nested objects also be edited? Is the operation a merge with predictable property precedence, a conversion between iterable values, or a true deep clone? These questions prevent the common mistake of choosing syntax first and discovering its semantics later.
Also check the input type. An array can be spread into an array or passed as arguments because it is iterable. A plain object can be spread into an object literal, but it is not automatically iterable for an array literal. If the value may be null, a class instance, a getter-backed object, or a large argument list, test that specific case rather than relying on the visual similarity of the three dots.
Finally, prefer the smallest operation that matches the contract. A shallow copy is often the clearest choice for a one-level immutable update. A targeted nested update can preserve sharing intentionally. A deep clone is appropriate only when the supported data types and independence requirement justify it. Copying everything by default can hide the real data model and make later changes harder to reason about.
A Small Test Matrix Beats a Slogan
When the behavior is unclear, write a small test that checks identity as well as values. Compare the outer container with the nested value: copy is original should usually be false, while copy[0] is original[0] may be true after a shallow array copy. For object updates, also check which property wins when two sources contain the same key. These tiny checks turn an assumption about the three dots into an observable contract and are easier to maintain than a broad promise that the result is “safe.”
FAQ
Is spread the same as rest?
No. Spread expands values in literals or call expressions; rest gathers values into a new array/object in parameters or destructuring. The syntax is identical-looking but opposite in direction.
Why did changing a nested value in my “copy” change the original?
Because spread/rest perform shallow copies: nested objects/arrays are referenced, not duplicated. Copy each level you will mutate or use a deep clone like structuredClone.
When should I prefer structuredClone over JSON.parse(JSON.stringify())?
Prefer structuredClone when you need a correct deep copy for Dates, Map/Set, typed arrays, ArrayBuffer, and circular structures. Use JSON for simple, JSON-compatible payloads only.
Will spread copy class instances and methods?
No. Spreading an instance copies its own enumerable properties into a plain object; methods on the prototype are not carried over. If you need instance-level copying, implement clone logic on the class or keep plain data structures for copying.
Is spread faster than a loop or structured clone?
Performance depends on data size and environment. Choose semantics first. If performance matters, benchmark realistic workloads in your runtime.

Alex Carter is the editorial name behind Vandutz Academy, a programming blog for beginners. Alex reviews and tests the examples and explanations published on the site, with a focus on making Python, JavaScript, web development, and developer tools easier to understand.