Destructuring and Spread
Prerequisites assumed: You are comfortable with objects, arrays, iteration, let/const, rest parameters, and the iterator protocol.
The mechanics
Destructuring is pattern matching against iterables and objects. The right-hand side is destructured; the left-hand side is the pattern. Object patterns bind properties by name; array patterns consume an iterable positionally, calling Symbol.iterator internally — so any iterable destructures, not just arrays.
const { name, ...rest } = { name: 'Ada', role: 'dev', level: 5 };
// name: 'Ada'; rest: { role: 'dev', level: 5 } — a fresh plain object
const [first, , third = 'n/a'] = [1, 2]; // holes skip; third hits its defaultDefaults only apply to undefined, never null or other falsy values. const { x = 1 } = { x: null } yields x === null. For true "absent or null" handling you need explicit fallbacks, e.g. x ?? 1 in a subsequent expression.
Nested patterns and renaming
Patterns nest arbitrarily, and the rename syntax is { source: target, nested: { a, b } }. A subtle gotcha: in const { port: p = 8080 }, the default belongs to p, not port. Assigning a default to a renamed binding looks like { port: p = 8080 } — reading it wrong is a common review bug.
const response = { data: { user: { id: 7, tags: ['admin'] } } };
const {
data: {
user: { id, tags: [primary] },
},
} = response; // id: 7, primary: 'admin'
// Note: `data` and `user` are NOT bound here — they were paths, not targets.Spread: copy, merge, and its limits
Spread at the call site or in literals copies own enumerable properties/elements one level deep. It invokes the iterator (arrays, strings, Maps, Sets) or CopyDataProperties (objects). Consequences:
- Spread of a string yields an array of code points, not UTF-16 code units — emoji spread correctly where
str.split('')does not. - Object spread runs getters on the source during the copy and silently drops non-enumerable and symbol-keyed... actually it does copy own enumerable symbols; it drops non-enumerables and prototype members.
- Both
{ ...a, ...b }andObject.assign({}, a, b)shallow-merge; neither deep-clones. Nested references are shared — a classic source of accidental mutation across "copies".
const base = { opts: { deep: true } };
const copy = { ...base };
copy.opts.deep = false;
console.log(base.opts.deep); // false — shared referenceRest, parameters, and evaluation order
Rest elements must be last in array patterns and parameters; trailing commas after rest are a syntax error. In function signatures, rest parameters capture leftovers (always a real array), and defaults are evaluated left to right — earlier parameters can seed later ones:
function area({ w, h = w * 2, unit = 'px' } = {}) {
return `${w}x${h} ${unit}`;
}Show diagram source
flowchart LR
A[Source value] --> B{Array pattern?}
B -- yes --> C[Get iterator, consume positions]
B -- no --> D[Read properties by name]
C --> E{Value undefined?}
D --> E
E -- yes --> F[Use default / rest]
E -- no --> G[Bind target]
F --> GExercise
Why does const [a, b] = 'hi😀' give a = 'h', b = 'i' with the emoji lost, and how do you fix it?
Show answer
Array patterns use the iterator, which walks code points — but with only two bindings, the emoji (a full code point pair 'i' + '😀'... actually 'i' and '😀' are two code points) is simply never bound. Fix it by binding all code points ([...str]) and slicing, or accept that the pattern stops after two positions. The point: destructuring consumes lazily by position; the emoji was at position 3, never requested.
Exercise
In { ...base, opts: { ...base.opts, deep: false } }, why is base.opts.deep unaffected?
Show answer
The nested spread creates a new opts object before mutation, breaking the shared reference. Shallow copying each level you intend to mutate is the standard workaround for the one-level-deep limit.
Spotted a mistake, or code that does not work?