跪拜 Guibai
← Back to the summary

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

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.