Two sides of the same syntax
ES2015 introduced two features that transformed how we work with arrays: destructuring,
which unpacks values out of an array into variables, and the spread operator, which
expands an array into individual elements. They share the ... token for the rest/spread
roles and together make array manipulation dramatically more concise. This guide covers both,
their interaction, and the patterns and pitfalls that matter day to day.
Array destructuring basics
Destructuring unpacks values by position using a pattern on the left of =.
const [first, second] = [10, 20]
first // 10
second // 20 assigned by index, not by name
It works on any iterable — arrays, strings, Sets, Maps, generators — not just arrays. That generality is why it's so widely useful.
Skipping, defaults, and swapping
You can skip positions with empty commas, supply defaults for missing values, and swap variables with no temp.
const [, , third] = ['a', 'b', 'c'] // skip first two -> 'c'
const [a = 1, b = 2] = [10] // a=10, b=2 (default for missing)
const [c = 5] = [null] // c=null default only for undefined
let x = 1, y = 2
;[x, y] = [y, x] // swap without a temp
Two subtleties: defaults apply only when the value is undefined (not null or other
falsy values), and the swap line should start with a semicolon if the previous statement
didn't end with one — otherwise [ is parsed as indexing.
Nested patterns and rest
Patterns can mirror nested structure, and a trailing rest element (...name) collects
the remaining values into a new array.
const [[a, b], [c]] = [[1, 2], [3]] // nested -> a=1, b=2, c=3
const [head, ...tail] = [1, 2, 3, 4]
head // 1
tail // [2, 3, 4] a real array
The rest element must be last — const [...rest, last] is a SyntaxError. It always
produces an array, even an empty one.
Destructuring in parameters and loops
Two of the most useful places for array destructuring are function parameters and for...of
loops — especially with entries() and Object.entries.
function distance([x1, y1], [x2, y2]) {
return Math.hypot(x2 - x1, y2 - y1)
}
distance([0, 0], [3, 4]) // 5
for (const [i, value] of arr.entries()) {
console.log(i, value) // index and value, cleanly
}
for (const [key, val] of Object.entries(obj)) { /* ... */ }
This keeps loop bodies free of clumsy pair[0]/pair[1] indexing.
The spread operator
Spread expands an iterable into individual elements — the inverse of a rest element. Its most common use is making a shallow copy or merging arrays.
const copy = [...arr] // shallow copy
const merged = [...a, ...b, ...c] // concatenate
const withExtras = [0, ...a, 99] // insert around elements
Element order follows source order, and you can freely interleave individual values with
spreads. It reads more clearly than a.concat(b, c), though concat can be marginally faster
for very large arrays.
Spread vs concat
Both merge arrays, but they have slightly different strengths. concat can append non-array
values directly and is often faster on huge arrays; spread is more readable and supports
interleaving.
[1, 2].concat(3, [4, 5]) // [1, 2, 3, 4, 5] — mixes values and arrays
[...[1, 2], 3, ...[4, 5]] // same result via spread
Use spread for clarity in normal code; reach for concat in performance-critical paths over
large arrays.
Spreading iterables and the emoji trap
Spread works on anything iterable: strings, Sets, Maps, arguments, NodeLists, generators.
For splitting a string into characters, prefer spread over split('') because spread iterates
by code point, correctly handling emoji and other astral characters.
[...'a😀b'] // ['a', '😀', 'b'] ✅
'a😀b'.split('') // ['a', '\ud83d', '\ude00', 'b'] ❌ broken surrogate pair
[...new Set([1, 1, 2])] // [1, 2] — dedupe in one line
Spreading a Set into an array is the idiomatic one-line way to deduplicate.
Passing arrays as arguments
Spread replaces the old Function.prototype.apply pattern for passing an array as separate
arguments.
const nums = [5, 1, 9]
Math.max(...nums) // 9
Math.max.apply(null, nums) // old, verbose equivalent
One caveat: spreading an extremely large array (100k+ elements) into a call can exceed
argument-count limits — use a loop or reduce for those cases.
The shallow-copy pitfall
The most common spread bug is assuming [...arr] deep-copies. It doesn't — nested
objects/arrays remain shared.
const matrix = [[1], [2]]
const copy = [...matrix]
copy[0].push(99)
matrix[0] // [1, 99] inner array was shared
For one level of nesting, map and spread each row (matrix.map(row => [...row])); for
arbitrary depth, use structuredClone(matrix).
Immutable updates with spread
Spread is the backbone of immutable array updates — building a new array around the change instead of mutating.
const added = [...arr, item] // append
const prepended = [item, ...arr] // prepend
const removed = [...arr.slice(0, i), ...arr.slice(i + 1)] // remove at index
These are the standard patterns in React/Redux state. The ES2023 with and toSpliced
methods cover replace/remove even more concisely.
A word on infinite iterables
Because destructuring pulls only as many values as the pattern needs, it works on infinite iterables — but a rest element would try to drain them and hang forever.
function* naturals() { let n = 1; while (true) yield n++ }
const [a, b, c] = naturals() // 1, 2, 3 only three pulled
// const [...all] = naturals() // infinite loop — never do this
Key takeaways
- Array destructuring unpacks by position from any iterable; supports skipping, defaults
(only for
undefined), nested patterns, and a trailing rest element. - It shines in function parameters and
for...ofloops, especially withentries(). - Spread expands iterables — use it to shallow-copy, merge, interleave, and pass arrays as arguments.
- Prefer spread over
split('')for characters (handles emoji) and[...new Set(arr)]to dedupe. - Spread copies are shallow — deep-update by copying the changed path or
structuredClone. - Destructuring is safe on infinite iterables, but never use a rest element on one.
Destructuring and spread together let you express unpacking, copying, and combining arrays in a single readable line — fluency with them is a hallmark of idiomatic modern JavaScript.