Spread with Arrays
The spread operator (...) expands an iterable — most commonly an array — into individual elements wherever a list of values is expected. Introduced in ES2015, it has become the idiomatic way to clone arrays, merge them, insert elements, and forward arguments to functions, all without mutating the originals. Because it favors creating new arrays over modifying existing ones, spread fits naturally into modern, immutable-by-default JavaScript. The one thing to keep in mind is that it copies shallowly — a caveat we cover at the end.
Cloning an array
Spreading an array into a new array literal produces a fresh array with the same elements. This is the cleanest way to copy an array so you can modify the copy without touching the source.
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // unchanged
console.log(copy);
console.log(original === copy); // different references
Output:
[ 1, 2, 3 ]
[ 1, 2, 3, 4 ]
false
This is equivalent to original.slice() but reads more declaratively, and it works on any iterable, not just arrays.
Merging and concatenating
Spread multiple arrays into one literal to concatenate them. Unlike Array.prototype.concat, you can freely interleave spread arrays with standalone values and control exactly where each piece lands.
const front = ["a", "b"];
const back = ["d", "e"];
const merged = [...front, "c", ...back];
console.log(merged);
Output:
[ 'a', 'b', 'c', 'd', 'e' ]
Inserting elements anywhere
Because spread is just a value expression inside an array literal, you can insert new elements at the start, middle, or end while keeping the result immutable.
const items = ["apple", "cherry"];
const withBanana = [...items.slice(0, 1), "banana", ...items.slice(1)];
console.log(withBanana);
Output:
[ 'apple', 'banana', 'cherry' ]
Spreading into function arguments
Spread can pass an array’s elements as separate positional arguments to a function. This replaces the old Function.prototype.apply pattern and works with constructors too.
const numbers = [5, 12, 3, 27, 8];
console.log(Math.max(...numbers));
console.log(Math.min(...numbers));
// Old way, no longer needed:
// Math.max.apply(null, numbers);
// Works with constructors:
const parts = [2026, 5, 1];
const date = new Date(...parts);
console.log(date.getFullYear());
Output:
27
3
2026
Tip: Don’t spread very large arrays into function calls. Each element becomes a separate argument, and engines cap how many arguments a call can take (often ~65k–500k depending on the runtime). For large datasets, use a method like
reduceinstead.
Turning a string into an array
Strings are iterable, so spreading one yields an array of characters. Crucially, spread iterates by Unicode code point, so it handles astral characters (like many emoji) correctly where String.prototype.split("") does not.
const text = "café";
console.log([...text]);
console.log([..."🚀⭐"]);
console.log("🚀⭐".split("")); // breaks surrogate pairs
Output:
[ 'c', 'a', 'f', 'é' ]
[ '🚀', '⭐' ]
[ '\ud83d', '\ude80', '⭐' ]
Spread vs. concat vs. push
These approaches overlap but differ in mutation behavior and ergonomics.
| Technique | Mutates source? | Returns | Best for |
|---|---|---|---|
[...a, ...b] | No | New array | Immutable merges, inserts, clones |
a.concat(b) | No | New array | Simple two-array joins, older codebases |
a.push(...b) | Yes (a) | New length | Appending into an existing array in place |
The shallow-copy caveat
Spread copies only one level deep. Elements that are objects or nested arrays are copied by reference, so mutating a nested value through the copy also affects the original.
const users = [{ name: "Ada" }, { name: "Linus" }];
const cloned = [...users];
cloned[0].name = "Grace"; // mutates the shared object
console.log(users[0].name); // affected!
console.log(cloned === users); // top level is a real copy
Output:
Grace
false
For a fully independent copy of nested data, use structuredClone:
const deepCopy = structuredClone(users);
deepCopy[0].name = "Margaret";
console.log(users[0].name); // safe
Output:
Grace
Warning:
structuredClonecannot copy functions, DOM nodes, or class instances (it throws aDataCloneError). For plain data it’s ideal; for richer objects, copy the relevant fields explicitly.
Best Practices
- Reach for
[...arr]to clone or merge arrays immutably instead of mutating withpush/splice. - Remember spread is shallow — use
structuredClonewhen you need independent nested objects. - Prefer
fn(...args)overfn.apply(null, args)for forwarding arguments; it’s clearer and supports constructors. - Use
[...str]rather thanstr.split("")when the string may contain emoji or other multi-byte characters. - Avoid spreading huge arrays into function calls; iterate or reduce instead to stay within argument limits.
- Combine spread with destructuring (
const [first, ...rest] = arr) to split arrays cleanly.