skip to content

Destructuring and Spread

Destructuring pulls values out of arrays positionally and spread pours them back in, which is how most modern JavaScript copies, merges, and swaps data. Interviewers ask about it because the copy it produces is shallow — a fact that quietly breaks nested-state updates.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In JavaScript array destructuring, when does a default value like `const [a = 5] = values` actually apply — and what do you get when the matching element is null, a hole, or missing entirely?

level: juniorimportance: must knowfreq 72%

answer

  1. one trigger value, not falsiness
  2. null is present, not absent
  3. missing index and holes read undefined
  4. initializers run lazily, left to right

basics

~20 s

A destructuring default fires only when the matched value is exactly undefined — a missing index, an array hole, or an explicit undefined. null, 0, empty string and NaN are real values, so they are bound as-is.

solid answer

~40 s

Destructuring defaults are an `undefined` check, not a falsy check. `const [a = 5] = [null]` binds `a` to `null`, because `null` is a value that was actually there; `const [a = 5] = []` and `const [a = 5] = [undefined]` both give `5`, because reading a missing index or an explicit `undefined` yields `undefined`. Array holes behave the same way — reading a hole produces `undefined`, so the default wins. Defaults are evaluated lazily, only when needed, and left to right, so a later default can reference an earlier binding: `const [x = 1, y = x + 1] = []` gives `1` and `2`. One more trap: the right-hand side must be iterable, so `const [a] = null` throws a TypeError rather than leaving `a` undefined.

go deeper

for a junior

Be able to state plainly that the default fires only for undefined, and to predict the output of a two-line snippet containing null and 0. Know the temp-free swap idiom.

for a middle

Explain that a hole and a missing index both read as undefined, that initializers evaluate lazily left to right and can see earlier bindings, and that a nullish source throws because destructuring needs an iterable.

for a senior

Show where this bites in real code: defaults on API payloads where the server sends null for 'no value', so the fallback silently never applies. Demonstrate normalizing the input or reaching for ?? instead of the pattern default.

for a principal

Own the convention question: decide whether your codebase represents absence as null or as a missing key, since destructuring defaults only work for one of them. An inconsistent choice across services is what makes these bugs recur.

## What a default actually tests Array destructuring is pattern matching against the values an iterator produces. For each target in the pattern the engine takes the next value from the source; if there is no next value, or the value produced is `undefined`, the target's initializer (the `= something` part) is evaluated and used instead. That is the entire rule, and it is worth saying out loud in an interview: **defaults trigger on `undefined` only** — never on any other falsy value. ```js const [a = 5] = []; // 5 — nothing at index 0 const [b = 5] = [undefined]; // 5 — explicit undefined const [c = 5] = [null]; // null — null is a real value const [d = 5] = [0]; // 0 const [e = 5] = ['']; // '' const [f = 5] = [NaN]; // NaN ``` Candidates who mentally translate `=` in a pattern into `||` get `c`, `d`, `e` and `f` wrong. If you genuinely want "fall back on any falsy value" you need `||`, and if you want "fall back on null or undefined" you need `??` — the destructuring default is the narrowest of the three. ## Holes and skipped positions A sparse array literal such as `[1, , 3]` has a hole at index 1: the array has length 3 but no own property named `"1"`. Reading that position yields `undefined`, so a default in the pattern applies: ```js const [p, q = 'gap', r] = [1, , 3]; // 1, 'gap', 3 ``` You can also deliberately skip positions by leaving the pattern slot empty. Commas advance the iterator without binding anything: ```js const [, second, , fourth] = ['a', 'b', 'c', 'd']; // 'b', 'd' ``` Skipping and defaulting are independent: an empty slot binds nothing at all, while `= value` binds a fallback. ## Lazy, left-to-right evaluation Initializers are not evaluated up front. They run only for the positions that need them, in pattern order, and each one sees the bindings already introduced to its left: ```js let calls = 0; const make = () => { calls++; return 'made'; }; const [x = make(), y = make()] = ['given']; // x === 'given', y === 'made', calls === 1 const [w = 1, z = w * 2] = []; // w === 1, z === 2 ``` This matters when a default is expensive or has a side effect: it is not paid for when the value is supplied. Referring *rightwards* does not work — `const [m = n, n = 1] = []` throws a ReferenceError because `n` is still in its temporal dead zone when `m`'s initializer runs. ## The source must be iterable Array destructuring calls the source's iterator. `null` and `undefined` have none, so the whole statement throws: ```js const [a] = null; // TypeError: null is not iterable const [a] = undefined; // TypeError const [a] = 42; // TypeError: 42 is not iterable ``` A default on the *pattern element* does not save you here — it only covers a missing element, not a missing source. When the source itself may be nullish, guard it: `const [a = 5] = values ?? []`. ## Swapping without a temporary The most quoted use of array destructuring is a swap, which works because the right-hand array is fully built before any assignment happens: ```js let a = 1, b = 2; [a, b] = [b, a]; // a === 2, b === 1 ``` The leading `[` is the classic gotcha: if the previous statement lacks a semicolon, automatic semicolon insertion joins the lines and the engine reads an index access instead. Start the line with a semicolon or keep semicolons everywhere. ## Interview framing The question is almost always asked as a snippet with a `null` or `0` in it, because that single case separates people who learned the rule from people who pattern-matched it onto `||`. Say the rule (`undefined` only), name the three ways `undefined` arises (missing index, hole, explicit `undefined`), and mention that the source must be iterable — that covers everything an interviewer is fishing for.

  • How would you fall back when the value can legitimately be null, not just undefined?
    A destructuring default will not help, because `null` is bound as-is. Apply `??` after the pattern — `const [raw] = values; const a = raw ?? 5;` — or destructure a normalized source. Use `||` only when you truly want every falsy value replaced, since it also swallows `0` and `''`.
  • Why does `[a, b] = [b, a]` swap correctly without a temporary variable?
    The right-hand side is evaluated to a complete array first, capturing the old values of both variables, and only then are the pattern targets assigned. Nothing reads a variable after it has been overwritten. Watch the leading bracket: without a semicolon ending the previous statement, ASI turns the line into an index access.
  • What happens if a default expression references a binding declared later in the same pattern?
    It throws a ReferenceError. Pattern bindings are initialized left to right, so a name to the right is still in its temporal dead zone when the earlier initializer runs. `const [m = n, n = 1] = []` fails, while `const [n = 1, m = n] = []` works and gives 1 and 1.

saying these in an interview costs you the question

  • Says the default applies to any falsy value
  • Claims null triggers the default
  • Thinks destructuring null yields undefined instead of throwing
  • Believes all default expressions are evaluated eagerly
  • Confuses a skipped comma slot with a default

context

open as a page

What exactly does the spread syntax copy in `const copy = [...items]` or `const next = {...state}`, and what bug appears once the data has a nested level?

level: middleimportance: must knowfreq 78%

basics

~20 s

Spread produces a one-level copy: the new array or object is a fresh container, but every nested array or object inside it is the very same reference as in the original. Mutating a nested value is visible through both.

open as a page

In JavaScript, what do `Array.from`, `Array.of`, `Array(n)` and array spread each produce, and which one do you use to build an array of n computed values?

level: middleimportance: should knowfreq 45%

basics

~10 s

Array(n) makes a length-n array with no elements at all, so array methods have nothing to visit. Array.from({length: n}, fn) fills those positions, Array.of(n) makes a single-element array, and spread converts anything iterable.

open as a page

In JavaScript destructuring, what does the rest element bind in `const [first, ...rest] = arr` and `const {id, ...others} = obj`, and what restrictions does the syntax place on it?

level: middleimportance: should knowfreq 60%

basics

~20 s

A rest element collects everything not matched by earlier targets into a brand-new array or object — an empty one when nothing remains, never undefined. It must be the last element in the pattern, takes no default, and copies only one level deep.

open as a page

A config object built as `{...defaults, ...userOptions}` loses nested defaults and, when the source was a class instance, its methods too. What does object spread actually copy, and why do both symptoms follow?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Object spread copies own enumerable properties, one level deep, later sources overwriting earlier ones per key. A nested key is replaced wholesale rather than merged, and nothing on the prototype — including class methods — comes along.

open as a page