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 Arrays

Array 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 lastconst [...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.