š¦ Destructuring & Spread Operator
Data in JavaScript lives in arrays and objects ā but reading and reshaping it used to take a lot of ceremony. ES6 gave us two syntaxes that changed everyday code: destructuring, which unpacks values out of a structure in one clean line, and the spread/rest ..., which copies, merges, and collects them just as easily.
Week 3 · Day 1 (Monday: ES6+ Features) · Lecture 2
šÆ Learning Objectives
By the end of this lesson, you will be able to:
- Destructure arrays and objects, including renaming, defaults, and nesting
- Destructure function parameters to write cleaner, self-documenting APIs
- Use the spread operator to copy and merge arrays and objects
- Distinguish the rest use of
...(collecting) from the spread use (expanding) - Write immutable update patterns the way React state demands
- Recognize the shallow-copy trap and know when you need a deep clone
Estimated Time: 60 minutes
Practice: Reshape an API response and build immutable shopping-cart operations.
In This Lesson
Unpacking Your Data
Picture a delivery box with several items inside. Without destructuring you lift each item out one at a time and set it down somewhere. Destructuring lets you unpack the whole box in a single move, naming each item as it comes out. The spread operator is the opposite motion ā it tips a box out into a bigger one, letting you copy and combine structures effortlessly.
Both are pure syntax sugar over things you could already do the long way ā but they make code so much shorter and clearer that they've become the default style across React, Node, and the whole ecosystem.
Array Destructuring
Array destructuring pulls values out by position. The variable names on the left line up with the elements on the right.
const colors = ['red', 'green', 'blue'];
// Bind by position
const [first, second, third] = colors;
console.log(first, second, third); // 'red' 'green' 'blue'
// Skip elements with an empty slot
const [primary, , tertiary] = colors;
console.log(primary, tertiary); // 'red' 'blue'
// Default values fill in when the source is short
const [a, b, c, d = 'yellow'] = colors;
console.log(d); // 'yellow'
// Collect the leftovers with a rest element
const [head, ...tail] = colors;
console.log(head); // 'red'
console.log(tail); // ['green', 'blue']
// A famously clean trick: swap two variables with no temp
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1
Object Destructuring
Object destructuring pulls values out by property name, not position ā so the order inside the braces doesn't matter, only the names.
const user = {
name: 'Alice',
age: 30,
email: 'alice@example.com',
address: { city: 'New York', country: 'USA' }
};
// Grab properties by name
const { name, age, email } = user;
// Rename as you extract ā property : newVariableName
const { name: userName, age: userAge } = user;
console.log(userName, userAge); // 'Alice' 30
// Default value for a property that may be missing
const { phone = 'N/A' } = user;
console.log(phone); // 'N/A'
// Reach into nested objects
const { address: { city, country } } = user;
console.log(city, country); // 'New York' 'USA'
// Rest gathers every remaining property into a new object
const { name: n, ...rest } = user;
console.log(rest); // { age: 30, email: '...', address: {...} }
š” Nested destructuring only reads
const { address: { city } } = user; creates city but not address ā the address: part is a path, not a new binding. If you want both, list them separately.
Destructuring Parameters
One of the highest-value uses is destructuring right in a function's parameter list. The signature becomes self-documenting ā a reader sees exactly which fields the function needs ā and defaults handle optional values.
// Instead of reaching into `user` inside the body...
function createUser({ name, email, role = 'user' }) {
console.log(`Creating ${name} (${email}) as ${role}`);
}
createUser({ name: 'Bob', email: 'bob@example.com' });
// "Creating Bob (bob@example.com) as user"
// Destructure a fetched response inline
async function loadUser() {
const res = await fetch('/api/user');
const { data: { user, preferences }, status } = await res.json();
console.log(user, preferences, status);
}
// This is exactly how React components read their props:
function UserProfile({ user, onEdit, onDelete }) {
// `user`, `onEdit`, `onDelete` are ready to use ā no props.xyz
}
ā ļø Guard against undefined
Destructuring undefined throws. If a function might be called with no argument, give the whole parameter a default: function f({ a } = {}) { ... }. The = {} means "if nothing was passed, destructure an empty object instead."
Spread with Arrays
The spread operator (...) expands an iterable into its individual elements wherever a list of values is expected. Think of spreading butter ā it takes something packed and lays it out flat.
// Copy an array (a fresh array, not a shared reference)
const original = [1, 2, 3];
const copy = [...original];
// Concatenate
const combined = [...[1, 2, 3], ...[4, 5, 6]]; // [1,2,3,4,5,6]
// Insert elements around a spread
const numbers = [2, 3, 4];
const more = [1, ...numbers, 5]; // [1,2,3,4,5]
// Turn any iterable into an array
const chars = [...'hello']; // ['h','e','l','l','o']
// Pass array items as separate arguments
console.log(Math.max(...[4, 1, 9, 3])); // 9
Spread with Objects
Object spread copies an object's own enumerable properties into a new object. When keys collide, the last one wins ā which is exactly what makes it perfect for applying overrides on top of defaults.
// Copy
const copied = { ...{ name: 'Alice', age: 30 } };
// Merge ā later sources override earlier ones
const defaults = { theme: 'light', notifications: true };
const userPrefs = { theme: 'dark' };
const settings = { ...defaults, ...userPrefs };
console.log(settings); // { theme: 'dark', notifications: true }
// Add or override a property while copying
const user = { name: 'Bob', age: 25 };
const updated = { ...user, age: 26, email: 'bob@example.com' };
console.log(updated); // { name: 'Bob', age: 26, email: 'bob@example.com' }
ā ļø Spread is a SHALLOW copy
const nested = { user: { name: 'Alice' } };
const shallow = { ...nested };
shallow.user.name = 'Bob';
console.log(nested.user.name); // 'Bob' ā the inner object is still shared!
Top-level properties are copied, but nested objects are shared by reference. To copy the inner objects too, spread them at each level, or use structuredClone(nested) (built into modern browsers and Node 17+) for a true deep clone.
Rest vs Spread ā same dots, opposite jobs
The ... token means two different things depending on where it sits. On the left of an = or in a parameter list it's rest ā it collects many values into one array or object. On the right, inside a call or literal, it's spread ā it expands one structure into many values.
// REST: gather trailing arguments into an array
function multiply(factor, ...numbers) {
return numbers.map(n => n * factor);
}
console.log(multiply(2, 1, 2, 3, 4)); // [2, 4, 6, 8]
// SPREAD: expand an array into those same arguments
const arr = [1, 2, 3, 4];
console.log(multiply(2, ...arr)); // [2, 4, 6, 8]
// Rest also works in destructuring (as we saw earlier)
const [firstItem, ...others] = [1, 2, 3, 4, 5];
console.log(others); // [2, 3, 4, 5]
Immutable Update Patterns
Modern front-end frameworks ā React above all ā expect you to treat state as immutable: never mutate the existing object, always produce a new one. Spread is the tool that makes this ergonomic. This is the single most common place you'll use it professionally.
// A Redux-style reducer builds a new state object each time
function userReducer(state = {}, action) {
switch (action.type) {
case 'UPDATE_USER':
return { ...state, ...action.payload };
case 'UPDATE_PROFILE':
return {
...state,
profile: { ...state.profile, ...action.payload }
};
default:
return state;
}
}
// The React useState functional-update pattern
setUser(prev => ({ ...prev, email: 'new@email.com' }));
// Immutable array helpers ā none of these mutate the input
const addItem = (arr, item) => [...arr, item];
const removeItem = (arr, i) => [...arr.slice(0, i), ...arr.slice(i + 1)];
const updateItem = (arr, i, newItem) => [...arr.slice(0, i), newItem, ...arr.slice(i + 1)];
ā ļø Don't spread inside a loop
// ā O(n²): rebuilds the whole array every iteration
let result = [];
for (const item of items) result = [...result, process(item)];
// ā
O(n): map, or push into one array
const result = items.map(process);
Practice & Quiz
šļø Exercise 1: Reshape an API response
Goal: The API returns snake_case keys and a nested preferences object. Use destructuring (with renaming and nesting) and spread to produce a flat, camelCase object.
const apiResponse = {
user_id: 123,
user_name: 'johndoe',
user_email: 'john@example.com',
created_at: '2023-01-01',
preferences: { theme: 'dark', notifications: true }
};
// Target:
// { id: 123, name: 'johndoe', email: 'john@example.com',
// createdAt: '2023-01-01', theme: 'dark', notifications: true }
function transformUserData(data) {
// your code
}
š” Hint
Destructure and rename in one shot: const { user_id: id, ... , preferences } = data;. Then destructure preferences and return an object that spreads nothing ā just lists the renamed values plus ... the prefs.
ā Solution
function transformUserData(data) {
const {
user_id: id,
user_name: name,
user_email: email,
created_at: createdAt,
preferences
} = data;
return { id, name, email, createdAt, ...preferences };
}
console.log(transformUserData(apiResponse));
šļø Exercise 2: Immutable cart update
Goal: Write updateQuantity(cart, itemId, quantity) that returns a new cart with the matching item's quantity changed ā without mutating the original.
const cart = {
items: [
{ id: 1, name: 'Book', price: 20, quantity: 1 },
{ id: 2, name: 'Pen', price: 5, quantity: 2 }
]
};
š” Hint
map over cart.items; for the matching id return { ...item, quantity }, otherwise return item unchanged. Then return { ...cart, items: newItems }.
ā Solution
function updateQuantity(cart, itemId, quantity) {
return {
...cart,
items: cart.items.map(item =>
item.id === itemId ? { ...item, quantity } : item
)
};
}
const next = updateQuantity(cart, 1, 3);
console.log(next.items[0].quantity); // 3
console.log(cart.items[0].quantity); // 1 ā original untouched
šÆ Quick Quiz
Question 1: What is tail after const [head, ...tail] = [1, 2, 3];?
Question 2: What does { ...{a:1, b:2}, b:9 } evaluate to?
Question 3: After const shallow = { ...obj }, you change a property on a nested object inside shallow. What happens to obj?
Best Practices & Pitfalls
ā Do
- Destructure function parameters to make signatures self-documenting
- Provide default values for properties that may be absent
- Use spread for immutable updates in React/Redux state
- Guard against destructuring
undefinedwith= {}on the parameter
ā Don't
- Assume spread deep-copies ā it's shallow; nested objects stay shared
- Spread inside a loop to build up an array (it's O(n²))
- Over-nest destructuring until it becomes unreadable ā split it into steps
- Forget that array destructuring is positional, so order matters
ā Deep clone when you truly need one
// Modern, built-in, handles nesting (Node 17+, all current browsers):
const deep = structuredClone(original);
// Older fallback ā loses functions, Dates become strings, etc.:
const deepJson = JSON.parse(JSON.stringify(original));
Summary
š Key Takeaways
- Array destructuring binds by position; object destructuring binds by name
- You can rename, supply defaults, and reach into nested structures
- Destructuring parameters (with
= {}as a guard) makes function APIs cleaner - Spread expands and copies; keys that collide are won by the last source
- Same dots, opposite jobs: rest collects, spread expands
- Spread is a shallow copy ā use
structuredClonefor deep clones - These patterns are the backbone of immutable state updates
š Additional Resources
š What's Next?
You can now reshape data with ease. The next lesson brings structure to your code itself: Classes and Modules ā organizing behavior into reusable, encapsulated units and splitting your app across files with import/export.
š Well done!
Destructuring and spread will appear in almost every component and reducer you write.