Array Destructuring & Spread Interview Questions & Answers
31 questions Updated 2026-06-18
JavaScript array destructuring and spread interview questions — defaults, skipping, swapping, nested patterns, rest elements, copying and merging arrays, spread vs concat and consuming iterables.
Read the in-depth guideJavaScript Array Destructuring & Spread — Unpacking, Copying and Combining ArraysArray destructuring unpacks values from an array into distinct
variables by position, using a pattern on the left of =.
const [first, second] = [10, 20]
first // 10
second // 20 assigned by index, not name
It works on any iterable, not just arrays — strings, Sets, Maps, and generators all destructure positionally.
Leave an empty slot (just a comma) for each element you want to skip.
const [, , third] = ['a', 'b', 'c']
third // 'c' first two skipped
Each comma consumes one position. It's readable for one skip; for several, consider naming with an underscore convention or indexing directly to keep it clear.
A default applies only when the matched value is undefined (not for
null or other falsy values).
const [a = 1, b = 2] = [10]
a // 10
b // 2 undefined -> default
const [c = 5] = [null]
c // null null is not undefined, no default
Missing array positions read as undefined, which is exactly why defaults
kick in there.
Build an array literal on the right and destructure it on the left — no temp variable needed.
let a = 1, b = 2
[a, b] = [b, a]
a // 2
b // 1
The right side is fully evaluated before assignment, so the swap is
atomic. Watch out: if the previous line lacks a semicolon, the leading
[ can be parsed as indexing — start the line with a semicolon if unsure.
Mirror the structure with nested patterns.
const [[a, b], [c]] = [[1, 2], [3]]
a // 1
b // 2
c // 3
You can mix in defaults and skips at any level. Deeply nested patterns get hard to read fast — destructure one level and name intermediates if it stops being obvious.
A trailing ...name collects all remaining elements into a new
array.
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 if empty.
Same syntax, opposite jobs. Rest collects multiple elements into one array (on the left, in a pattern). Spread expands one iterable into multiple elements (on the right, in a literal or call).
const [first, ...rest] = arr // rest: gather
const copy = [...arr] // spread: expand
Position is the tell: ... on the binding side = rest; ... on the value
side = spread.
[...arr] produces a shallow copy — a new top-level array whose
elements are the same references.
const a = [{ x: 1 }, 2]
const b = [...a]
b === a // false new array
b[0] === a[0] // true inner object still shared
For nested data you need a deep copy: structuredClone(a).
Spread each array into a new literal, in the order you want.
const merged = [...a, ...b, ...c] // flat concatenation
const withExtras = [0, ...a, 99] // insert around them
It reads more clearly than a.concat(b, c) and lets you interleave
individual elements, though concat can be marginally faster for very
large arrays.
Functionally similar for merging, but concat has two edge behaviors
spread doesn't: it can append non-array values directly, and it's often
faster for huge arrays since the engine doesn't iterate element-by-element.
[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 readability and interleaving; reach for concat in hot
paths over large arrays.
Anything iterable: strings, Set, Map, arguments, NodeLists,
generators.
[...'abc'] // ['a', 'b', 'c'] string -> chars
[...new Set([1,1,2])] // [1, 2]
[...map.entries()] // [[k, v], ...]
Plain objects are not iterable, so [...{a:1}] throws. Use
Object.values/entries to turn an object into an iterable first.
Spread iterates by code point (respecting surrogate pairs), while
split('') splits by UTF-16 code unit, breaking emoji and other
astral characters.
[...'a😀b'] // ['a', '😀', 'b'] ✅
'a😀b'.split('') // ['a', '\ud83d', '\ude00', 'b'] ❌ broken
Prefer spread (or Array.from) whenever input might contain non-BMP
characters.
Spread it in the call site — it replaces the old Function.prototype.apply
pattern.
const nums = [5, 1, 9]
Math.max(...nums) // 9
Math.max.apply(null, nums) // old way
You can also mix fixed and spread args: fn(a, ...rest, b). One caveat:
spreading a very large array (100k+) can hit argument-count limits — loop
or reduce instead.
They look identical but live in different places. A rest element appears in a destructuring pattern and gathers leftover array items; rest parameters appear in a function signature and gather leftover arguments.
const [a, ...rest] = arr // rest element
function f(first, ...others) {} // rest parameters
Both produce real arrays (unlike the old arguments object), and both must
be last.
Put the pattern directly in the parameter list — handy for functions
receiving coordinate pairs or Object.entries items.
function dist([x1, y1], [x2, y2]) {
return Math.hypot(x2 - x1, y2 - y1)
}
dist([0, 0], [3, 4]) // 5
Add a default for the whole parameter (= []) if it might be missing,
otherwise destructuring undefined throws.
Destructure each iterated item inline — extremely common with
entries() and Object.entries.
for (const [i, value] of arr.entries()) {
console.log(i, value) // index and value
}
for (const [key, val] of Object.entries(obj)) { /* ... */ }
It keeps the loop body clean versus indexing into a pair with [0]/[1].
Destructure the two positions on the left and provide them swapped on the right.
const a = [1, 2, 3, 4]
;[a[1], a[3]] = [a[3], a[1]]
a // [1, 4, 3, 2]
As with variable swaps, lead with a semicolon when the previous statement
doesn't end in one, so [ isn't read as indexing.
Yes — defaults are evaluated left to right, so a later default can use an already-assigned earlier one.
const [a = 1, b = a * 2] = []
a // 1
b // 2 b's default used a
Referencing a variable that hasn't been bound yet (to its right) throws a temporal-dead-zone error. Keep dependencies left-to-right.
Both throw a TypeError, because destructuring tries to read the
iterator (Symbol.iterator) off the value.
const [a] = null // TypeError
const [b] = undefined // TypeError
const [c] = [] // c is undefined (empty but iterable)
Guard with a default at the source: const [c] = maybeArr ?? [].
People assume [...arr] deep-copies, but inner arrays/objects stay shared,
so mutating them mutates the "copy" too.
const matrix = [[1], [2]]
const copy = [...matrix]
copy[0].push(99)
matrix[0] // [1, 99] original changed
For grids/nested structures use matrix.map(row => [...row]) (one level)
or structuredClone(matrix) (any depth).
Array.from accepts a mapping function and works on array-like
objects (those with length but no Symbol.iterator), which spread can't
handle.
Array.from({ length: 3 }, (_, i) => i) // [0, 1, 2] array-like + map
[...{ length: 3 }] // TypeError, not iterable
Use spread for iterables and brevity; use Array.from for array-likes or
when you want to map during creation.
Build a new array around the change instead of mutating.
const added = [...arr, item] // append
const prepend = [item, ...arr] // prepend
const removed = [...arr.slice(0, i), ...arr.slice(i + 1)] // remove at i
This is the standard pattern for React/Redux state. The ES2023
arr.with(i, val) and toSpliced cover replace/remove even more cleanly.
Yes — elements land in source order, so later items can override or follow earlier ones.
[...a, ...b] // a's items first, then b's
[...b, ...a] // reversed grouping
Unlike object spread (where later keys win), array spread doesn't "override" — every element is kept; only the resulting positions differ.
Yes — they compose freely, as long as rest stays last.
const [, second = 0, ...rest] = [1]
second // 0 (skipped first, defaulted missing second)
rest // [] nothing left
Powerful but easy to over-pack; if a single pattern needs all three, it's often clearer to split into a couple of statements.
A function returns an array (or tuple-like) and the caller destructures it
positionally — the pattern behind React's useState.
function minMax(arr) {
return [Math.min(...arr), Math.max(...arr)]
}
const [lo, hi] = minMax([3, 1, 9]) // lo=1, hi=9
Use array returns when order is meaningful and stable; prefer an object return when callers should pick fields by name.
Wrap in a Set (drops duplicates by SameValueZero), then spread back to an
array.
const unique = [...new Set([1, 1, 2, 3, 3])] // [1, 2, 3]
It preserves first-seen order and is the idiomatic one-liner. Note it dedupes by reference for objects, so identical-looking objects won't collapse.
For a known, small number of arrays, yes. For an arbitrary array of
arrays, prefer flat or reduce.
[...a, ...b] // fine for a fixed set
[[1],[2],[3]].flat() // [1, 2, 3] arbitrary length
rows.reduce((acc, r) => [...acc, ...r], []) // works but O(n²)
The reduce-with-spread version reallocates each step — use flat() or
acc.push(...r) for performance.
Spread the array into Math.max/Math.min.
Math.max(...[3, 9, 1]) // 9
Math.min(...[3, 9, 1]) // 1
Caveat: Math.max() of an empty array is -Infinity (and min is
Infinity), and spreading a massive array may exceed argument limits — use
reduce for those cases.
Either skip with an empty slot, or bind to a throwaway name like _.
const [, , third] = list // skip slots
const [_a, _b, third2] = list // name-but-ignore convention
The underscore is just a normal variable (linters often allow an _
prefix to mean "unused"). Skipping slots avoids creating bindings at all.
Destructuring pulls only as many values as the pattern needs and then stops — so it works on infinite iterables.
function* naturals() { let n = 1; while (true) yield n++ }
const [a, b, c] = naturals()
a, b, c // 1, 2, 3 only three pulled
But a rest element (const [first, ...rest]) would try to drain the
whole iterable and hang forever — never use rest on an infinite source.
Functionally identical; performance is essentially the same in modern engines, though a destructuring swap allocates a small array literal that the optimizer usually elides.
[a, b] = [b, a] // readable, idiomatic
const t = a; a = b; b = t // traditional, zero allocation
Prefer the destructuring form for clarity; only the traditional swap in extremely hot numeric loops where you've profiled an actual difference.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.