Module: JavaScript
JavaScript·057·8 MIN READ

057: Destructuring, Rest, Spread, and Copy Semantics

TOPICS COVERED: Destructuring, Rest, Spread, and Copy Semantics

Learning outcomes

By the end of this lesson, you can:

  • destructure arrays and objects, rename bindings, and provide defaults;
  • distinguish rest collection from spread expansion;
  • copy and merge arrays and objects without changing their outer containers;
  • predict which value wins when object spreads contain the same key;
  • explain why spread creates a shallow, not deep, copy.

Retrieval warm-up

  1. How do you read a key held in const field = "price"?
  2. What does const alias = product copy?
  3. Does toSorted() copy each product object?

Answers: product[field], an object reference, and no.

Vocabulary

  • Destructuring: Unpacking array items/object properties into distinct bindings. — Source: MDN: Destructuring assignment
  • Binding: Name created by destructuring patterns in declarations/parameters. — Source: MDN: Destructuring assignment
  • Default value: = fallback used when unpacked slot is undefined. — Source: MDN: Destructuring assignment
  • Rename: : newName syntax rebinding a property under a different local name. — Source: MDN: Destructuring assignment
  • Rest: …gathers remaining elements/properties into one target. — Source: MDN: Rest parameters
  • Spread: …expands iterables/properties into receivers. — Source: MDN: Spread syntax
  • Shallow copy: New outer container sharing nested references ({...obj}). — Source: MDN: Spread syntax
  • Merge: Later spread sources overriding earlier keys during combination. — Source: MDN: Spread syntax
  • Destructuring (official): "Destructuring assignment unpacks values from arrays or properties from objects into distinct variables." — Source: MDN: Destructuring assignment
  • Spread syntax (official): "Spread (...) allows an iterable to be expanded in places where zero or more arguments or elements are expected." — Source: MDN: Spread syntax

Beginner mental model

The same ... punctuation performs opposite jobs depending on location.

  • On the receiving, left side, rest collects: const [first, ...others] = values.
  • Inside a new array/object or call, spread expands: const copy = [...values].

Destructuring is a concise form of property or position access:

js
const product = { id: "p1", name: "Notebook", price: 4 };
const { name, price } = product;
// Equivalent reads: const name = product.name; const price = product.price;

const coordinates = [120, 80];
const [x, y] = coordinates;

Object destructuring matches keys; order does not matter. Array destructuring follows iteration order; position matters. Rename with property: localName:

js
const { name: productName } = product;

This does not create a variable named name; it creates productName.

Defaults apply only when the value is undefined, including a missing property. They do not replace null, 0, false, or "":

js
const { stock = 0 } = { stock: undefined }; // 0
const { rating = 5 } = { rating: null };    // null

Worked beginner example: prepare a catalog card

js
const product = {
  id: "p3",
  name: "Water Bottle",
  price: 16,
  stock: 7,
  category: "travel",
  details: {
    color: "blue",
    dimensions: { height: 24, width: 7 },
  },
};

const {
  id,
  name: productName,
  price,
  rating = "Not rated",
  ...catalogData
} = product;

const card = {
  id,
  title: productName,
  displayPrice: `$${price}`,
  rating,
};

const tags = ["reusable", "popular"];
const allTags = ["travel", ...tags, "bpa-free"];
const [primaryTag, ...secondaryTags] = allTags;

console.log(card);
console.log(catalogData.stock);
console.log(primaryTag);
console.log(secondaryTags);
console.log(product.name);

Output:

text
{ id: "p3", title: "Water Bottle", displayPrice: "$16", rating: "Not rated" }
7
travel
["reusable", "popular", "bpa-free"]
Water Bottle

catalogData is a new object containing enumerable own properties not already selected: stock, category, and details. It does not change product. secondaryTags is a new array. Destructuring reads or collects values; it does not delete properties from the source.

Use destructured function parameters when the function needs a few known fields:

js
function formatProduct({ name, price, stock = 0 }) {
  return `${name} - $${price} (${stock} available)`;
}

console.log(formatProduct(product));
// Water Bottle - $16 (7 available)

For an optional whole argument, add an object default: function readOptions({ limit = 10 } = {}). Without = {}, calling readOptions() tries to destructure undefined and throws.

Copying and merging precisely

Array spread creates a new outer array:

js
const originalTags = ["new", "travel"];
const copiedTags = [...originalTags];
copiedTags.push("sale");
console.log(originalTags); // ["new", "travel"]

Object spread creates a new plain object from enumerable own properties. Later definitions overwrite earlier values:

js
const defaults = { currency: "USD", taxRate: 0.1, region: "global" };
const storeSettings = { taxRate: 0.18, region: "IN" };

const settings = { ...defaults, ...storeSettings, currency: "INR" };
console.log(settings);
// { currency: "INR", taxRate: 0.18, region: "IN" }

Read left to right. storeSettings.taxRate wins over the default; the final explicit currency wins over both.

The shallow-copy boundary

Spread copies the outer property list, not recursively nested objects:

js
const copy = { ...product };
console.log(copy === product);                 // false
console.log(copy.details === product.details); // true

copy.details.color = "green";
console.log(product.details.color); // green

To update one nested level without changing the original, copy every container along that path:

js
const greenProduct = {
  ...product,
  details: {
    ...product.details,
    color: "green",
  },
};

Now greenProduct.details !== product.details. The still-deeper dimensions object remains shared because it was not changed. 047 develops this path-copying pattern.

Spread is not a universal clone. For this curriculum, do not reach for JSON serialization as a "deep clone"; it loses or changes unsupported values. Copy only the levels the update requires. structuredClone() exists for supported data when a true deep clone is genuinely required, but it is not a substitute for understanding data ownership.

Deep copy choices and JSON limitations

There are three different operations that interviews often collapse into the word "copy": sharing the original reference, making a shallow copy, and making a deep copy. Spread and Object.assign() only copy the first level. Path copying is usually the best choice for an immutable update because it documents the changed branch and preserves structural sharing elsewhere.

structuredClone(value) recursively clones many built-in data types and preserves cycles, but it has boundaries: functions and DOM nodes are not cloneable, and prototype-based class instances should not be assumed to retain their behavior. It also creates entirely new nested identities, which can be more work than a targeted update.

JSON round-tripping is not a general deep-clone algorithm:

js
const source = {
  date: new Date("2024-01-01T00:00:00Z"),
  missing: undefined,
  amount: 12n,
};

// JSON.stringify(source) throws because BigInt cannot be serialized.
// Without amount, undefined object properties disappear and date becomes text.

JSON also cannot represent Map, Set, functions, symbols, NaN, or infinity faithfully; array undefined values become null, and circular data throws. Use JSON only for a deliberately JSON-shaped wire format, not to clone arbitrary application state. Choose path copying for a known update and structuredClone when a supported value genuinely needs independent recursive ownership.

Interview follow-ups

  • What does { ...source } guarantee? A new outer object, not independent nested values.
  • When is structuredClone inappropriate? When the graph contains unsupported values or behavior must remain attached to class instances.
  • Why not always deep clone? It discards useful identity sharing and can be expensive.
  • What should be tested? Both displayed values and identity at each relevant level.

Intermediate example: update product options

js
function updateProduct(product, changes) {
  return {
    ...product,
    ...changes,
    id: product.id,
  };
}

const discounted = updateProduct(product, {
  price: 14,
  badge: "Weekend deal",
  id: "attempted-change",
});

console.log(discounted.price); // 14
console.log(discounted.badge); // Weekend deal
console.log(discounted.id);    // p3
console.log(product.price);    // 16

The order encodes a rule: callers may change ordinary fields, but the original ID is written last and cannot be overridden. Do not blindly spread external data into security-sensitive or unrestricted records; validate allowed fields first.

Array and object destructuring combine naturally with callbacks:

js
const cartEntries = [["p1", 2], ["p3", 1]];
const labels = cartEntries.map(
  ([productId, quantity]) => `${productId}: ${quantity}`,
);

Optional advanced extension

Remove a property immutably with object rest:

js
function withoutInternalNote(product) {
  const { internalNote, ...publicProduct } = product;
  return publicProduct;
}

const safe = withoutInternalNote({
  id: "p1",
  name: "Notebook",
  internalNote: "supplier review",
});
console.log(safe); // { id: "p1", name: "Notebook" }

This creates a shallow object; any nested values are still shared.

Common mistakes and debugging

  • Confusing rename syntax: { name: title } creates title, not name.
  • Expecting defaults for null: defaults apply only to undefined.
  • Rest not last: a rest element/property must be last; an array rest element cannot have a trailing comma.
  • Spreading a plain object into an array: [...plainObject] throws because a normal object is not iterable.
  • Wrong overwrite order: inspect spreads left to right and place protected values last.
  • Assuming deep copy: compare nested references with copy.details === original.details.
  • Mutating a nested shared value: copy each object/array from the root to the changed field.
  • Over-destructuring: deeply nested patterns can hide the data shape. Use intermediate variables when clearer.

Best practices

  • Destructure fields that improve readability; do not unpack every property automatically.
  • Use domain names when renaming, such as name: productName.
  • Use rest to omit known fields and spread to build a new container.
  • Treat all spread/rest copies as shallow unless demonstrated otherwise.
  • Make merge precedence intentional and visible.
  • Validate external changes before merging them.

Checkpoint

Use three cards labeled root product, details, and dimensions. For each spread in the nested update, replace the corresponding card and leave every other card in place. Predict === at each level. Repeat with only { ...product } and observe that the deeper cards remain shared. This concrete path-copy exercise prevents the misleading rule "spread makes a copy" from becoming an assumption of deep cloning.

Read three merge expressions from left to right and identify the final writer of each duplicate key. Include valid falsy defaults (0, false, and "") to reinforce that destructuring defaults respond only to undefined.

Exercises

Core

Destructure name as title, price, and a default stock of 0 from { name: "Cable", price: 9 }.

js
const source = { name: "Cable", price: 9 };
const { name: title, price, stock = 0 } = source;
console.log(title, price, stock);

Output: Cable 9 0

Practice

Merge default checkout settings with user settings. Ensure currency always remains "INR".

js
const defaults = { currency: "INR", delivery: "standard", giftWrap: false };
const userSettings = { currency: "USD", giftWrap: true };
const checkoutSettings = {
  ...defaults,
  ...userSettings,
  currency: "INR",
};

console.log(checkoutSettings);

Output:

text
{ currency: "INR", delivery: "standard", giftWrap: true }

Professional Extension

Update product.details.dimensions.width to 8 without mutating product. Prove the root, details, and dimensions are new, while the original width remains 7.

js
const widerProduct = {
  ...product,
  details: {
    ...product.details,
    dimensions: {
      ...product.details.dimensions,
      width: 8,
    },
  },
};

console.log(widerProduct.details.dimensions.width); // 8
console.log(product.details.dimensions.width);      // 7
console.log(widerProduct !== product);              // true
console.log(widerProduct.details !== product.details); // true
console.log(
  widerProduct.details.dimensions !== product.details.dimensions,
); // true

Recap

Explain rest versus spread from position alone. When does a destructuring default run? Which value wins in { ...a, ...b }? Why can changing copy.details.color change original.details.color? Describe the containers that must be copied for a nested update.

Official references

Deep-copy decision table

Choose a copy based on the data contract, not on the word "deep":

NeedChoiceCyclesImportant edge cases
Share one value intentionallyoriginal referenceyesMutations are shared
Immutable update at a known pathspread/rest path copyingn/aBest identity/performance; unchanged branches stay shared
Clone supported arbitrary datastructuredClone(value)yesClones Date, Map, Set, typed arrays; rejects functions, DOM nodes, and some host values
Serialize JSON-shaped dataJSON.stringify/JSON.parsenoDrops undefined object keys/functions/symbols; converts dates to text; NaN/Infinity to null; BigInt and cycles throw
Preserve behavior/custom instancesexplicit domain clone()dependsDefine exactly which fields and prototype behavior survive
js
const graph = { date: new Date("2024-01-01T00:00:00Z"), map: new Map([["x", 1]]) };
graph.self = graph;
const copy = structuredClone(graph);
console.assert(copy !== graph && copy.self === copy);
console.assert(copy.date instanceof Date && copy.map instanceof Map);

const jsonReady = { name: "Notebook", tags: ["paper"] };
const jsonCopy = JSON.parse(JSON.stringify(jsonReady));
console.assert(jsonCopy.tags !== jsonReady.tags);

structuredClone is not automatically the best answer: it recursively allocates the whole graph, loses class methods/prototype behavior for unsupported domain semantics, and cannot clone functions. JSON round-tripping is useful at a wire boundary only when the contract is intentionally JSON. Test values and identity, including undefined, null, dates, cycles, maps, sets, BigInt, and class instances, before selecting an approach.

Copy-choice interview questions

  1. What references change in a path-copy update, and which remain shared?
  2. Why does JSON cloning turn a Date into a string and fail on a cycle?
  3. Which clone preserves a cycle and a Map without custom code?
  4. Why is a domain-specific clone safer for a class with methods or invariants?
  5. Write tests that distinguish a new root from independent nested ownership.