JavaScript Spread Operator
The JavaScript spread operator, written as ..., expands values into a place that expects individual items or properties. It is wonderfully convenient and slightly dangerous. Spread can make copying and merging look effortless, but it only creates shallow copies. If nested objects are involved, the top level is new and the deeper references are still shared.
That is the difference between "this copied" and "this copied enough for the bug to wait until Friday."
Spread in arrays
Array spread expands an iterable inside an array literal:
const start = [1, 2];
const end = [3, 4];
const combined = [...start, ...end];
console.log(combined); // [1, 2, 3, 4]
You can mix spread values with individual values:
const numbers = [2, 3];
const result = [1, ...numbers, 4];
console.log(result); // [1, 2, 3, 4]
The order is exactly the order written.
Copying arrays
Spreading an array into a new array literal gives you a new top-level array:
const original = ["red", "green"];
const copy = [...original];
copy.push("blue");
console.log(original); // ["red", "green"]
console.log(copy); // ["red", "green", "blue"]
That is useful when you want to avoid mutating the original array.
It is not a deep copy:
const users = [{ name: "Ada" }, { name: "Grace" }];
const copy = [...users];
copy[0].name = "Changed";
console.log(users[0].name); // "Changed"
The array is new. The objects inside it are the same objects.
Merging arrays
Spread replaces many old concat calls:
const published = ["closures", "arrays"];
const drafts = ["fetch", "modules"];
const allPosts = [...published, ...drafts];
It does not remove duplicates:
const a = [1, 2, 3];
const b = [2, 3, 4];
const merged = [...a, ...b];
console.log(merged); // [1, 2, 3, 2, 3, 4]
Use Set when uniqueness is the goal:
const unique = [...new Set([...a, ...b])];
console.log(unique); // [1, 2, 3, 4]
Spread in function calls
Function-call spread expands an iterable into positional arguments:
const scores = [88, 94, 72];
console.log(Math.max(...scores)); // 94
Before spread, you often saw apply for this:
Math.max.apply(null, scores);
Spread makes the call site clearer. The array is unpacked into normal arguments.
Spread in objects
Object spread copies own enumerable properties into a new object:
const user = {
id: 1,
name: "Ada",
};
const copy = { ...user };
You can add or override properties:
const updated = {
...user,
name: "Ada Lovelace",
active: true,
};
console.log(updated);
// { id: 1, name: "Ada Lovelace", active: true }
The last property wins. That makes order important:
const defaults = { theme: "light", pageSize: 20 };
const userPrefs = { pageSize: 50 };
const settings = { ...defaults, ...userPrefs };
console.log(settings);
// { theme: "light", pageSize: 50 }
Spread defaults first, then overrides. Reverse the order and your defaults will overwrite the user, which is a bold design choice and usually a bad one.
Shallow copy is the main caveat
Object spread is shallow too:
const original = {
name: "Ada",
profile: {
city: "London",
},
};
const copy = { ...original };
copy.profile.city = "Paris";
console.log(original.profile.city); // "Paris"
If you update nested data, copy each level you change:
const updated = {
...original,
profile: {
...original.profile,
city: "Paris",
},
};
If you need a full deep copy of supported data types, structuredClone() may be the right tool:
const deepCopy = structuredClone(original);
Do not use JSON stringify/parse as a casual deep-copy tool unless you are happy to lose functions, undefined, dates, maps, sets, and several other perfectly real values.
Object spread is not iterable spread
These two forms look similar but work differently:
const arr = [..."abc"];
const obj = { ...{ a: 1, b: 2 } };
Array and function-call spread require iterables:
const chars = [..."abc"];
const fromSet = [...new Set([1, 2, 3])];
Plain objects are not iterable:
const config = { host: "localhost" };
[...config];
// TypeError: config is not iterable
Object spread copies properties inside an object literal:
const configCopy = { ...config };
Same token, different context.
Object.assign versus spread
Object.assign is still common in older code:
const merged = Object.assign({}, defaults, userPrefs);
The spread version is usually easier to read:
const merged = { ...defaults, ...userPrefs };
The easy mistake with Object.assign is forgetting the empty target:
Object.assign(defaults, userPrefs);
That mutates defaults. Object spread always creates a new object literal, so there is no target object to accidentally mutate.
Key Takeaways
- Spread expands array items, iterable values, function-call arguments, or object properties depending on context.
- Array and object spread create new top-level containers.
- Spread copies are shallow; nested objects remain shared unless you copy those levels too.
- Object merge order matters because the last property wins.
- Use spread for readable copying and merging, but use
structuredClone()or explicit nested copies when deep independence matters.