057: 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
- How do you read a key held in
const field = "price"? - What does
const alias = productcopy? - 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:
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:
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 "":
const { stock = 0 } = { stock: undefined }; // 0
const { rating = 5 } = { rating: null }; // null
Worked beginner example: prepare a catalog card
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:
{ 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:
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:
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:
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:
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:
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:
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
structuredCloneinappropriate? 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
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:
const cartEntries = [["p1", 2], ["p3", 1]];
const labels = cartEntries.map(
([productId, quantity]) => `${productId}: ${quantity}`,
);
Optional advanced extension
Remove a property immutably with object rest:
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 }createstitle, notname. - Expecting defaults for
null: defaults apply only toundefined. - 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 }.
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".
const defaults = { currency: "INR", delivery: "standard", giftWrap: false };
const userSettings = { currency: "USD", giftWrap: true };
const checkoutSettings = {
...defaults,
...userSettings,
currency: "INR",
};
console.log(checkoutSettings);
Output:
{ 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.
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
- MDN: Destructuring
- MDN: Spread syntax
- MDN: Shallow copy
- ECMA-262: Destructuring Assignment
- ECMA-262: Object Initializer
- MDN:
structuredClone() - MDN: JSON serialization
Deep-copy decision table
Choose a copy based on the data contract, not on the word "deep":
| Need | Choice | Cycles | Important edge cases |
|---|---|---|---|
| Share one value intentionally | original reference | yes | Mutations are shared |
| Immutable update at a known path | spread/rest path copying | n/a | Best identity/performance; unchanged branches stay shared |
| Clone supported arbitrary data | structuredClone(value) | yes | Clones Date, Map, Set, typed arrays; rejects functions, DOM nodes, and some host values |
| Serialize JSON-shaped data | JSON.stringify/JSON.parse | no | Drops undefined object keys/functions/symbols; converts dates to text; NaN/Infinity to null; BigInt and cycles throw |
| Preserve behavior/custom instances | explicit domain clone() | depends | Define exactly which fields and prototype behavior survive |
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
- What references change in a path-copy update, and which remain shared?
- Why does JSON cloning turn a
Dateinto a string and fail on a cycle? - Which clone preserves a cycle and a
Mapwithout custom code? - Why is a domain-specific clone safer for a class with methods or invariants?
- Write tests that distinguish a new root from independent nested ownership.
