// Copy array (shallow)
const original = [1, 2, 3];
const copy = [...original];
// Merge arrays
const merged = [...[1, 2], ...[3, 4]]; // [1, 2, 3, 4]
// Add elements at specific positions
const withExtra = [0, ...original, 4]; // [0, 1, 2, 3, 4]
// Convert iterable to array
const chars = [...'hello']; // ['h', 'e', 'l', 'l', 'o']
const unique = [...new Set([1, 1, 2])]; // [1, 2]
// Function arguments
const nums = [3, 1, 2];
Math.max(...nums); // 3
// Convert NodeList to array
const divs = [...document.querySelectorAll('div')];
Spread creates a shallow copy — nested objects and arrays are still shared references. For deep copies, use structuredClone(). The spread operator works with any iterable (arrays, strings, Sets, Maps, generators), not just arrays.
Why it matters: The spread operator is ubiquitous in modern JavaScript — used in every React codebase for state updates, array manipulation, and component props. Understanding its shallow-copy limitation prevents subtle reference-sharing bugs.
Real applications: React state immutable updates ([...list, newItem]), merging API responses with defaults, converting NodeLists to arrays for array methods, and passing arrays of arguments to variadic functions like Math.max(...nums).
Common mistakes: Assuming spread creates a deep copy (it doesn't — nested objects are shared references), spreading non-iterables (objects can't be spread into arrays), and spreading inside template literals expecting it to join with commas (use join() instead).
// Copy object (shallow)
const user = { name: 'Alice', age: 30 };
const copy = { ...user };
// Merge objects (later properties win)
const defaults = { theme: 'light', lang: 'en' };
const prefs = { theme: 'dark' };
const config = { ...defaults, ...prefs };
// { theme: 'dark', lang: 'en' }
// Add or override properties
const updated = { ...user, age: 31, role: 'admin' };
// { name: 'Alice', age: 31, role: 'admin' }
// Shallow copy caveat
const nested = { a: { b: 1 } };
const clone = { ...nested };
clone.a.b = 99;
console.log(nested.a.b); // 99 — shared reference!
// Spread only copies own enumerable properties
// Prototype properties and non-enumerable properties are excluded
Object spread is commonly used for immutable updates in state management (React, Redux). The pattern { ...state, key: newValue } creates a new object with one property changed. Remember that order matters — properties spread later override earlier ones with the same key.
Why it matters: Object spread is the standard pattern for immutable state updates in React and Redux. Every setState call that modifies nested state, and every Redux reducer, uses this pattern. Understanding that it's shallow prevents state mutation bugs.
Real applications: Redux reducers (return { ...state, count: state.count + 1 }), React useState updaters, HTTP request option overrides ({ ...defaults, ...customOptions }), and merging configuration objects in Node.js applications.
Common mistakes: Assuming object spread does a deep merge (it replaces entire nested objects), spreading to "update" a nested property ({ ...obj, nested.prop: val } is invalid — use { ...obj, nested: { ...obj.nested, prop: val } }), and not knowing prototype and non-enumerable properties are excluded from spread.
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // 6
// With leading parameters
function log(level, ...messages) {
messages.forEach(m => console.log('[' + level + ']', m));
}
log('INFO', 'started', 'ready');
// [INFO] started
// [INFO] ready
// Rest vs arguments
function oldWay() {
// arguments is array-like, not a real array
const args = Array.from(arguments);
// No arrow function support for arguments
}
function newWay(...args) {
// args is a real array
args.map(a => a * 2); // works directly
args.filter(a => a > 0); // all array methods work
}
// Rest must be last
// function bad(...rest, last) {} // SyntaxError
Rest parameters replaced the arguments object in modern JavaScript. Key advantages: rest params are a real Array (no conversion needed), work with arrow functions (arguments does not), can collect a subset of arguments, and clearly signal the function's intent in the signature.
Why it matters: Rest parameters enable clean variadic function APIs without the arguments object hack. The fact that arrow functions lack arguments — making rest params the only option — is a common interview question about arrow function limitations.
Real applications: Logging utilities (log(level, ...messages)), Redux middleware (store => next => action =>), functional composition helpers (pipe(...fns)), and any function that needs to accept a flexible number of arguments with array methods available.
Common mistakes: Placing rest parameters before other parameters (SyntaxError — must be last), using arguments in arrow functions (it doesn't exist — use rest), and treating rest params as optional when they're always an array (empty array if no extra args, never undefined).
const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1 2 3
// Skip elements with commas
const [first, , third] = [10, 20, 30];
console.log(first, third); // 10 30
// Rest element collects remaining
const [head, ...tail] = [1, 2, 3, 4];
console.log(head); // 1
console.log(tail); // [2, 3, 4]
// Default values for missing elements
const [x = 0, y = 0] = [42];
console.log(x, y); // 42, 0
// From function return values
function getRange() { return [1, 10]; }
const [min, max] = getRange();
// From regex match groups
const [, year, month] = '2024-03'.match(/(d{4})-(d{2})/);
console.log(year, month); // "2024" "03"
// Works with any iterable
const [firstChar] = 'hello'; // "h"
const [firstItem] = new Set([10, 20]); // 10
Array destructuring is particularly useful for function return values (returning multiple values as an array), regex match results, and swapping variables. The rest element must be the last element in the pattern — you cannot have elements after it.
Why it matters: Array destructuring is how React's useState and useReducer hooks return paired values. Understanding the syntax is essential for reading React code and for returning multiple values from functions without an object wrapper.
Real applications: React hooks (const [state, setState] = useState()), Promise.allSettled result processing, regex match group extraction, coordinate swapping, and any API that returns tuples (like Go-style [result, error] patterns).
Common mistakes: Forgetting commas to skip elements (const [,second] = arr), assuming missing elements throw an error (they produce undefined), and placing rest elements anywhere other than last in the pattern (SyntaxError).
const user = { name: 'Alice', age: 30, role: 'admin' };
// Basic destructuring
const { name, age } = user;
console.log(name, age); // "Alice" 30
// Rename variables
const { name: userName, role: userRole } = user;
console.log(userName); // "Alice"
// Default values
const { name: n, score = 0 } = user;
console.log(score); // 0 (not in object)
// Rest — collect remaining properties
const { name: nm, ...rest } = user;
console.log(rest); // { age: 30, role: "admin" }
// Computed property names
const key = 'name';
const { [key]: value } = user;
console.log(value); // "Alice"
// Destructuring from existing variables (note the parentheses)
let a, b;
({ a, b } = { a: 1, b: 2 }); // parentheses needed!
Object destructuring is the foundation of named parameters in JavaScript. Instead of remembering argument order, functions can accept an options object and destructure it. The rest syntax (...rest) is useful for extracting specific properties while forwarding all others — a common pattern in React for component props.
Why it matters: Object destructuring is the most-used ES6 feature in modern JavaScript. Every React component using props, every function taking an options object, and every API response handler uses it. Knowing rename and default syntax is essential.
Real applications: React function components (function Card({ title, body, onClick })), destructuring fetch response bodies, extracting specific fields from database query results, and forwarding props in component composition (const { className, ...rest } = props).
Common mistakes: Destructuring without parentheses when assigning to existing variables ({ a, b } = obj is parsed as a block — needs ({ a, b } = obj)), confusing rename syntax ({ orig: newName }) with object shorthand, and not providing = {} default when destructuring an optional parameter.
const data = {
user: {
name: 'Alice',
address: { city: 'NYC', zip: '10001' }
},
scores: [95, 88, 72]
};
// Nested object destructuring
const { user: { name, address: { city } } } = data;
console.log(name, city); // "Alice" "NYC"
// Nested array
const { scores: [first, ...others] } = data;
console.log(first); // 95
console.log(others); // [88, 72]
// Array of objects
const people = [{ name: 'Bob' }, { name: 'Eve' }];
const [{ name: name1 }, { name: name2 }] = people;
console.log(name1, name2); // "Bob" "Eve"
// Safe nested destructuring with defaults
const { user: { phone = 'N/A' } = {} } = data;
console.log(phone); // "N/A"
// Very deep nesting (use sparingly)
const { a: { b: { c: { d } } } } = { a: { b: { c: { d: 42 } } } };
console.log(d); // 42
While nested destructuring is powerful, deeply nested patterns become hard to read. If you are destructuring more than 2-3 levels deep, consider extracting intermediate variables instead. Also note that the parent variable is not created — in { user: { name } }, only name is declared, not user.
Why it matters: Nested destructuring is common when processing GraphQL/REST responses with nested data. Knowing that intermediate bindings aren't created prevents "user is not defined" errors, and using safe defaults (= {}) prevents "cannot destructure property of undefined" crashes.
Real applications: Processing nested API responses, extracting config values from deep configuration trees, React component props with nested data structures, and destructuring event.target fields in event handlers (const { target: { name, value } } = event).
Common mistakes: Deep nesting making code unreadable (introduce intermediate variables), not using = {} defaults for potentially absent nested objects (causes TypeError), and being surprised that the parent in { user: { name } } = obj doesn't create a user variable.
// Object defaults
const { x = 10, y = 20, z = 30 } = { x: 1, y: 2 };
console.log(x, y, z); // 1, 2, 30
// Only undefined triggers defaults — NOT null
const { a = 'default' } = { a: null };
console.log(a); // null (NOT "default")
const { b = 'default' } = { b: undefined };
console.log(b); // "default"
// Default with rename
const { name: n = 'Anonymous' } = {};
console.log(n); // "Anonymous"
// Default can reference other destructured values
const { width = 100, height = width } = { width: 50 };
console.log(width, height); // 50, 50
// Default with function call (only called if needed)
const { value = expensiveComputation() } = { value: 42 };
// expensiveComputation() is NOT called — value exists
// Array defaults
const [first = 'none', second = 'none'] = ['hello'];
console.log(first, second); // "hello", "none"
Defaults are lazily evaluated — if the value is present, the default expression is never executed. This is important for defaults that involve function calls or complex computations. Note that null is not undefined — assigning null to a property will NOT trigger the default value.
Why it matters: The null vs undefined distinction for defaults is a classic interview trap. React form handlers often see null from APIs where you'd expect defaults to kick in — they don't. This distinction is also why optional chaining uses ?? (not ||) for defaults.
Real applications: Function parameter defaults, config object defaults, React prop defaults (alternative to defaultProps), and processing optional fields in API responses where missing fields come as undefined vs explicitly null fields from the server.
Common mistakes: Expecting defaults to apply for null (only undefined triggers defaults), not realizing defaults can reference previous parameters (const { w, h = w }), and using expensive function calls as defaults without realizing they're evaluated each time the default is needed.
const response = {
data: { user_name: 'Alice', is_active: true },
status_code: 200
};
// Rename to camelCase
const {
data: { user_name: userName, is_active: isActive },
status_code: statusCode
} = response;
console.log(userName); // "Alice"
console.log(isActive); // true
console.log(statusCode); // 200
// Rename with default
const { color: bg = 'white' } = {};
console.log(bg); // "white"
// Rename from reserved words or special names
const { class: className, for: htmlFor } = element.attributes;
// Multiple properties to same prefix
const { width: w, height: h, depth: d } = dimensions;
Renaming is especially useful when working with API responses that use snake_case — you can convert to camelCase during destructuring. The syntax reads as "take property original and assign it to variable newName." The colon here does not mean type annotation — it means "rename to."
Why it matters: Renaming during destructuring is the idiomatic way to adapt snake_case API responses to camelCase JavaScript conventions in a single line. It's also essential for avoiding collisions with existing variable names in scope.
Real applications: Converting REST API snake_case fields to camelCase (const { user_name: userName }), extracting DOM event properties (const { target: { id: elementId } } = event), and renaming imported module bindings for clarity.
Common mistakes: Reading { prop: alias } as "prop is an alias" (it's the reverse — assign prop to variable alias), forgetting to use the renamed variable afterward (accidentally using the original name), and combining rename with default incorrectly ({ prop: alias = default } is the correct syntax).
// Object parameter destructuring
function createUser({ name, age, role = 'user' }) {
return { name, age, role };
}
createUser({ name: 'Alice', age: 30 });
// With full default for the entire parameter
function connect({ host = 'localhost', port = 3000 } = {}) {
console.log(host + ':' + port);
}
connect(); // "localhost:3000"
connect({ port: 8080 }); // "localhost:8080"
// Array parameter destructuring
function first([head]) { return head; }
first([1, 2, 3]); // 1
// Nested destructuring in parameters
function getCity({ address: { city } }) { return city; }
getCity({ address: { city: 'NYC' } }); // "NYC"
// Rest in parameters
function logAll({ name, ...rest }) {
console.log(name);
console.log(rest); // everything except name
}
The = {} default after the destructuring pattern is critical — without it, calling connect() with no arguments throws a TypeError because you cannot destructure undefined. This two-level defaulting (default for the whole parameter, plus defaults for individual properties) is the standard pattern.
Why it matters: Parameter destructuring is the idiomatic pattern for options objects in modern JavaScript. Every well-written utility function and every React component uses it. The = {} default for the entire parameter is a specific pattern interviewers look for.
Real applications: React function components (function Card({ title, onClick, className = '' })), Node.js utility functions with optional config, Express middleware options, and any public API that accepts configuration objects with reasonable defaults.
Common mistakes: Not providing = {} default for the whole parameter (calling the function with no args throws TypeError), trying to access arguments in an arrow function with rest params (use rest params — arguments doesn't exist in arrows), and confusing parameter destructuring with variable destructuring syntax.
// Swap two variables
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2, 1
// Swap three variables (rotate)
let x = 'a', y = 'b', z = 'c';
[x, y, z] = [z, x, y];
console.log(x, y, z); // "c", "a", "b"
// Swap array elements
const arr = [1, 2, 3, 4];
[arr[0], arr[3]] = [arr[3], arr[0]];
console.log(arr); // [4, 2, 3, 1]
// Without destructuring (old way)
let temp = a;
a = b;
b = temp;
// Swap in sorting algorithms
function bubbleSort(arr) {
for (let i = 0; i < arr.length; i++)
for (let j = 0; j < arr.length - 1 - i; j++)
if (arr[j] > arr[j + 1])
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
return arr;
}
The destructuring swap works because the right side is evaluated first (creating a temporary array), then the left side assigns the values. This is syntactic sugar — the engine still uses temporary storage internally. It is widely used in sorting algorithms and state management.
Why it matters: The destructuring swap is a canonical example of how destructuring improves code readability. It appears in algorithm interviews (bubble sort, quicksort partition) and is a quiz staple. Understanding that the right side is evaluated atomically prevents mental model errors.
Real applications: Sorting algorithms that require in-place swaps, React state that needs two values swapped (e.g., column sort order toggle), matrix transpositions, and any utility code that needs to exchange variable values without a temp variable.
Common mistakes: Using the swap on object properties without arr[] notation (works fine), expecting the temporary array to be visible or GC-relevant (it's engine-optimized away), and using an assignment statement without let/const when using the swap outside of a declaration context.
// SPREAD — expands/unpacks elements
const arr = [1, 2, 3];
console.log(...arr); // 1 2 3 (expanded)
const copy = [...arr]; // [1, 2, 3] (spread into new array)
Math.max(...arr); // 3 (spread as arguments)
const obj = { a: 1 };
const clone = { ...obj }; // { a: 1 } (spread into new object)
// REST — collects/packs elements
function sum(...nums) { // rest parameter (collects arguments)
return nums.reduce((a, b) => a + b, 0);
}
const [first, ...remaining] = [1, 2, 3, 4];
// first = 1, remaining = [2, 3, 4] (rest element)
const { name, ...others } = { name: 'A', age: 1, role: 'B' };
// name = 'A', others = { age: 1, role: 'B' } (rest properties)
// Quick rule:
// Left side of = or in parameters → REST (collecting)
// Right side of = or in arguments → SPREAD (expanding)
A simple way to remember: if ... appears where values are expected (right side of assignment, function arguments), it is spread. If it appears where names are expected (left side of assignment, function parameters), it is rest. Rest must always be the last element.
Why it matters: Spread vs rest is one of the most commonly confused ES6 topics in interviews. Both use ... but do opposite things — being able to clearly explain the difference and identify which is which at a glance demonstrates deep ES6 understanding.
Real applications: Rest in variadic functions and destructuring, spread in React state updates and function calls. The same ... appears in all these contexts — the mental model of "expanding vs collecting" is what unifies them.
Common mistakes: Thinking spread and rest are different operators (same syntax, different contexts), attempting to use rest in the middle of a parameter list (SyntaxError), and confusing spread in an object literal with the object spread proposal for merging.
// String destructuring
const [a, b, c] = 'ABC';
console.log(a, b, c); // "A" "B" "C"
// Map destructuring
const map = new Map([['name', 'Alice'], ['age', 30]]);
for (const [key, value] of map) {
console.log(key, value);
}
// "name" "Alice"
// "age" 30
// Set destructuring
const [first, second] = new Set([10, 20, 30]);
console.log(first, second); // 10 20
// Generator destructuring
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const [f1, f2, f3, f4, f5] = fibonacci();
console.log(f1, f2, f3, f4, f5); // 0 1 1 2 3
// Custom iterable
const range = {
*[Symbol.iterator]() {
for (let i = 1; i <= 5; i++) yield i;
}
};
const [x, y, ...rest] = range; // x=1, y=2, rest=[3,4,5]
Destructuring with generators only consumes as many values as you request — the generator pauses after yielding the last needed value. This is lazy evaluation — useful for extracting a few values from an infinite sequence without computing the whole thing.
Why it matters: Knowing that destructuring works with any iterable (not just arrays) unlocks powerful patterns with Maps, Sets, and generators. The for...of [key, value] pattern for Map iteration is standard and critical to know.
Real applications: Map key-value iteration (for (const [key, val] of map)), generator-based lazy sequences, processing entries from Object.entries(), and extracting results from regexp match iterators (string.matchAll()).
Common mistakes: Trying to destructure a non-iterable plain number or boolean (TypeError), not realizing generator destructuring is lazy (safe for infinite sequences), and confusing map.entries() with Object.entries(map) (use the former for Maps).
// Shallow merge with spread (top-level only)
const defaults = { theme: 'light', fonts: { body: 'Arial', heading: 'Georgia' } };
const custom = { theme: 'dark', fonts: { body: 'Helvetica' } };
const shallow = { ...defaults, ...custom };
// { theme: 'dark', fonts: { body: 'Helvetica' } }
// fonts.heading is LOST — entire fonts object was replaced
// Deep merge utility
function deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = deepMerge(target[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
const deep = deepMerge(defaults, custom);
// { theme: 'dark', fonts: { body: 'Helvetica', heading: 'Georgia' } }
// Deep clone with structuredClone
const cloned = structuredClone(defaults);
cloned.fonts.body = 'Comic Sans';
console.log(defaults.fonts.body); // "Arial" — independent
structuredClone() handles circular references, Date, RegExp, Map, Set, and ArrayBuffer correctly. JSON.parse(JSON.stringify()) loses functions, undefined values, Dates (become strings), and fails on circular references. For deep merging in production, consider libraries like lodash.merge.
Why it matters: The shallow-copy limitation of spread is one of the most common sources of React state bugs. Developers think they're updating nested state immutably with spread, but they're sharing references to nested objects. Knowing structuredClone() vs. JSON.parse/stringify tradeoffs is a senior-level signal.
Real applications: Deep-cloning Redux state for unit tests, copying nested config objects before modification, implementing undo/redo by taking full state snapshots, and merging user preference objects over application defaults without losing nested keys.
Common mistakes: Using { ...obj } and assuming nested objects are cloned (they're shared), using JSON.parse(JSON.stringify()) on objects with Dates or functions (they're lost), and not using spread order correctly when merging ({ ...defaults, ...overrides } gives overrides priority).
// Spread as function arguments
const numbers = [3, 1, 4, 1, 5];
Math.max(...numbers); // 5
Math.min(...numbers); // 1
// Replaces apply
// Old: Math.max.apply(null, numbers)
// New: Math.max(...numbers)
// Multiple spreads in one call
const first = [1, 2];
const second = [3, 4];
console.log(...first, ...second); // 1 2 3 4
// With new — spread works, apply does not
const dateArgs = [2024, 0, 15]; // Jan 15, 2024
const date = new Date(...dateArgs);
// new Date.apply(null, dateArgs) — does NOT work
// Spread string into console.log
console.log(...'hello'); // h e l l o
// Pass Map entries as arguments
function showEntry(key, value) {
console.log(key + ': ' + value);
}
const entries = new Map([['name', 'Alice']]);
for (const entry of entries) {
showEntry(...entry); // "name: Alice"
}
Spread in function calls is syntactically cleaner than apply and has two major advantages: it works with new (constructors), and you can mix spread with regular arguments: fn(a, ...arr, b). With apply, you could only pass one array.
Why it matters: Spread in function calls is the modern replacement for Function.prototype.apply(). Knowing it works with new (which apply cannot do) and can be mixed with regular args in any position demonstrates deep ES6 understanding.
Real applications: Math.max(...nums), Array.prototype.push(...items) to add multiple items, constructing Date objects from arrays of arguments, and passing Map entries directly to functions (fn(...entry)).
Common mistakes: Still using .apply(null, args) instead of spread (works but is legacy), spreading large arrays (millions of elements) as function args (stack overflow risk — use chunked loops), and forgetting spread only works with iterables (plain objects can't be spread in function calls).
// Props destructuring
function UserCard({ name, age, role = 'user', ...rest }) {
// rest contains all other props (className, style, etc.)
return '' + name + '';
}
// useState destructuring
const [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
// useReducer
const [state, dispatch] = useReducer(reducer, initialState);
// Custom hook returning object
function useUser(id) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
return { user, loading };
}
const { user, loading } = useUser(1);
// Context destructuring
const { theme, toggleTheme } = useContext(ThemeContext);
// Event object destructuring
function handleChange({ target: { name, value } }) {
setForm(prev => ({ ...prev, [name]: value }));
}
// Spread props to child components
function Button({ children, ...rest }) {
return '';
}
The spread props pattern (...rest) is used to forward all unhandled props to a child element. This is the foundation of wrapper components in React — extract the props you need, spread the rest to the underlying DOM element or child component.
Why it matters: React is the dominant use case for destructuring in most frontend codebases. These patterns appear in every React project — props destructuring, useState array destructuring, context destructuring. Fluency with them is non-negotiable for React interviews.
Real applications: Every React function component, every custom hook, controlled form inputs (const { target: { name, value } } = event), and HOCs that pass through all props to wrapped components all use these patterns daily.
Common mistakes: Forgetting that useState returns an array (use array destructuring, not object), not forwarding unknown props in wrapper components (breaks accessibility attributes like aria-*), and over-spreading props exposing internal props to DOM elements (causes React warnings about unknown HTML attributes).