Destructuring and Spread: The JavaScript Shortcuts That Break on null and Shallow Copy
Destructuring Assignment and Spread Operator: Modern JS Efficiency Techniques
1. Introduction: Why Modern JS Can't Do Without Destructuring and Spread
ES6's destructuring assignment and spread operator are among the most frequently used syntactic sugar in daily JavaScript. They don't change the capability boundary of the language, but they drastically cut boilerplate, compressing high-frequency operations like "extracting values, passing arguments, merging, copying" from five or six lines into one.
This article revolves around five core points: array destructuring, object destructuring, default values, rest parameters, and spread copying. It provides ready-to-run examples and common pitfalls.
The knowledge map can be drawn in one diagram:
flowchart TD
A["Destructuring and Spread"] --> B["Destructuring: how to extract elegantly"]
A --> C["Spread: how to merge and copy elegantly"]
B --> B1["Array destructuring: extract by position"]
B --> B2["Object destructuring: extract by property name"]
B --> B3["Default values: only undefined takes effect"]
B --> B4["Rest syntax: collect the rest"]
C --> C1["Shallow copy and merge"]
C --> C2["Shallow copy boundary and structuredClone"]
B1 --> D["Common pitfalls checklist"]
B2 --> D
B3 --> D
B4 --> D
C1 --> D
C2 --> D
2. Array Destructuring
Array destructuring extracts values by position, essentially calling the iterator, so any iterable (arrays, strings, Sets, Maps, generators) can be destructured.
const arr = [1, 2, 3, 4, 5];
const [first, second] = arr;
console.log(first, second); // 1 2
// Skip elements: use empty slots
const [, , third] = arr;
console.log(third); // 3
// Swap variables without a temp variable
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
// Strings destructure too
const [c1, c2] = 'JS';
console.log(c1, c2); // J S
Nested array destructuring just needs matching structure on both sides:
const matrix = [[1, 2], [3, 4]];
const [[x1, y1], [x2, y2]] = matrix;
console.log(x1, y1, x2, y2); // 1 2 3 4
3. Object Destructuring
Object destructuring extracts by property name, order-independent. That's the key difference from array destructuring.
const user = {
id: 1,
name: 'Alice',
profile: { city: 'Shanghai', age: 28 }
};
// Basic destructuring
const { name, id } = user;
console.log(name, id); // Alice 1
// Rename: originalName: newVariableName
const { name: userName } = user;
console.log(userName); // Alice
// Nested destructuring
const { profile: { city } } = user;
console.log(city); // Shanghai
Function parameter destructuring is one of the most practical uses of object destructuring. It makes "config object" calls clear and avoids writing options.xxx repeatedly in the function body:
function createButton({ text = '按钮', type = 'primary', disabled = false } = {}) {
return `<button type="${type}" ${disabled ? 'disabled' : ''}>${text}</button>`;
}
console.log(createButton({ text: '提交' }));
// <button type="primary" >提交</button>
Note the = {} at the end: it ensures calling without arguments doesn't throw Cannot destructure property ... of undefined.
4. Default Values
Default values only apply when the destructured result is undefined. null, 0, '', and false do not trigger defaults — this is the most common pitfall.
const [p = 10] = [];
console.log(p); // 10
const [q = 10] = [null];
console.log(q); // null, not 10
const { count = 0 } = { count: undefined };
console.log(count); // 0
const { flag = true } = { flag: false };
console.log(flag); // false
Default values can be expressions, and they are lazy evaluated: only executed when needed.
function getDefault() {
console.log('计算默认值');
return 100;
}
const { value = getDefault() } = { value: 1 };
// Doesn't print "计算默认值" because value already has a value
console.log(value); // 1
Default values can reference previously destructured variables:
const { width, height = width } = { width: 300 };
console.log(width, height); // 300 300
5. Rest Parameters and Rest Properties
Rest syntax uses ... to collect "the rest". In function parameters it's called rest parameters; in destructuring, rest properties.
// Rest parameter: must be the last formal parameter
function sum(first, ...rest) {
return rest.reduce((acc, cur) => acc + cur, first);
}
console.log(sum(1, 2, 3, 4)); // 10
// Array rest elements
const [head, ...tail] = [1, 2, 3, 4];
console.log(head, tail); // 1 [2, 3, 4]
// Object rest properties: often used to strip fields
const { password, ...safeUser } = { id: 1, name: 'Alice', password: '123456' };
console.log(safeUser); // { id: 1, name: 'Alice' }
The difference between arguments and rest parameters is worth remembering: arguments is an array-like object, not a real array, and it contains all arguments. Rest parameters are real arrays, only contain the part not matched by earlier formal parameters, and work in arrow functions too.
6. Spread Operator and Copying
The spread operator uses ... to "flatten" an iterable. Its most common use is shallow copy and merging.
// Array shallow copy
const source = [1, 2, 3];
const copy = [...source];
console.log(copy); // [1, 2, 3]
console.log(copy === source); // false
// Array merging
const merged = [...source, 4, 5];
console.log(merged); // [1, 2, 3, 4, 5]
// Object shallow copy and override
const base = { a: 1, b: 2 };
const next = { ...base, b: 3, c: 4 };
console.log(next); // { a: 1, b: 3, c: 4 }
// Spreading in function calls
console.log(Math.max(...source)); // 3
6.1 Shallow Copy Boundaries
Spread copying only goes one level deep; nested objects still share references:
const original = { user: { name: 'Alice' } };
const shallow = { ...original };
shallow.user.name = 'Bob';
console.log(original.user.name); // Bob, original got changed
For deep copy, use structuredClone (modern browsers and Node 17+):
const deep = structuredClone(original);
deep.user.name = 'Carol';
console.log(original.user.name); // Bob, original unaffected
6.2 Choosing Between Spread and Destructuring
- Need to "extract by position" → array destructuring;
- Need to "extract by name" → object destructuring;
- Need to "collect the rest" → rest syntax;
- Need to "create a new object/array" → spread copying.
Turn the above into a decision flow chart for quick reference:
flowchart TD
S["Got data, what to do?"] --> Q1{"Extract by position?"}
Q1 -->|Yes| R1["Array destructuring: const [a, b] = arr"]
Q1 -->|No| Q2{"Extract by name?"}
Q2 -->|Yes| R2["Object destructuring: const { name } = obj"]
Q2 -->|No| Q3{"Collect the rest?"}
Q3 -->|Yes| R3["Rest syntax: ...rest"]
Q3 -->|No| Q4{"Create new object or array?"}
Q4 -->|Yes| R4["Spread copy: [...arr] / { ...obj }"]
R4 --> Q5{"Has nested references?"}
Q5 -->|Yes| R5["structuredClone for deep copy"]
Q5 -->|No| R6["Shallow copy is fine"]
7. Common Pitfalls Checklist
| Scenario | Wrong | Correct | Note |
|---|---|---|---|
Destructuring null/undefined |
const { a } = null |
const { a } = obj ?? {} |
Throws TypeError |
| Default value misjudgment | const { n = 1 } = { n: null } |
Explicitly check n ?? 1 |
null does not trigger default |
| Rest parameter position | function f(...rest, last) |
function f(first, ...rest) |
Rest must be last |
| Object spread override | { ...a, ...b } wrong order |
Later write wins | Order decides precedence |
| Deep copy misuse | { ...obj } modify nested |
structuredClone(obj) |
Spread copies only one level |
8. Summary
Destructuring solves "how to extract elegantly"; spread solves "how to merge and copy elegantly". Combined with defaults and rest syntax, most data-moving code compresses to a single line while staying readable. Only two things need vigilance: defaults don't apply to null, and spread copying is always shallow.