The three dots (...) in JavaScript serve two powerful purposes: as the rest operator, it collects multiple values into an array; as the spread operator, it expands arrays and objects into individual elements. Despite using the same syntax, these operators are opposites—one gathers, one spreads. They're essential for modern JavaScript, enabling flexible function parameters, easy array/object manipulation, and clean code patterns. Let's master both uses of this versatile operator!
Spread Operator (...)
The spread operator expands (spreads) an iterable into individual elements.
1. Spreading Arrays
// Spreading arrays into individual elements
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
// Old way: concat
let combined1 = arr1.concat(arr2);
console.log(combined1); // [1, 2, 3, 4, 5, 6]
// Modern way: spread
let combined2 = [...arr1, ...arr2];
console.log(combined2); // [1, 2, 3, 4, 5, 6]
// Mix spread with other elements
let mixed = [0, ...arr1, 3.5, ...arr2, 7];
console.log(mixed); // [0, 1, 2, 3, 3.5, 4, 5, 6, 7]
// Spread in function calls
let numbers = [5, 3, 9, 1];
console.log(Math.max(...numbers)); // 9
// Same as: Math.max(5, 3, 9, 1)
// Old way without spread
console.log(Math.max.apply(null, numbers)); // Ugly!2. Cloning Arrays
let original = [1, 2, 3];
// Wrong: This only copies the reference!
let notACopy = original;
notACopy.push(4);
console.log(original); // [1, 2, 3, 4] - original changed!
// Correct: Spread creates a shallow clone
let copy = [...original];
copy.push(5);
console.log(original); // [1, 2, 3, 4] - unchanged
console.log(copy); // [1, 2, 3, 4, 5]
// Other cloning methods (all equivalent)
let copy2 = original.slice();
let copy3 = Array.from(original);
let copy4 = [...original]; // Cleanest!
// Note: Shallow clone (nested arrays still reference same objects)
let nested = [[1, 2], [3, 4]];
let shallowCopy = [...nested];
shallowCopy[0].push(99);
console.log(nested); // [[1, 2, 99], [3, 4]] - changed!
console.log(shallowCopy); // [[1, 2, 99], [3, 4]]3. Spreading Objects
// Spread object properties into new object
let person = { name: 'Alice', age: 25 };
let address = { city: 'NYC', zip: '10001' };
// Combine objects
let user = { ...person, ...address };
console.log(user);
// { name: 'Alice', age: 25, city: 'NYC', zip: '10001' }
// Clone objects
let personCopy = { ...person };
personCopy.age = 30;
console.log(person.age); // 25 (unchanged)
console.log(personCopy.age); // 30
// Override properties (last one wins)
let defaults = { theme: 'light', size: 'medium' };
let userPrefs = { theme: 'dark' };
let settings = { ...defaults, ...userPrefs };
console.log(settings);
// { theme: 'dark', size: 'medium' }
// userPrefs.theme overrides defaults.theme
// Add properties while spreading
let enhanced = {
...person,
email: 'alice@test.com',
active: true
};
console.log(enhanced);
// { name: 'Alice', age: 25, email: 'alice@test.com', active: true }Spread makes code cleaner
Without Spread
// Old way: Manual copying
// Combine arrays
let arr1 = [1, 2];
let arr2 = [3, 4];
let combined = arr1.concat(arr2);
// Clone array
let copy = arr.slice();
// Combine objects
let obj = {};
for (let key in obj1) {
obj[key] = obj1[key];
}
for (let key in obj2) {
obj[key] = obj2[key];
}
// Or Object.assign
let obj = Object.assign({}, obj1, obj2);
// Pass array to function
Math.max.apply(null, numbers);With Spread
// Modern way: Spread operator
// Combine arrays
let arr1 = [1, 2];
let arr2 = [3, 4];
let combined = [...arr1, ...arr2];
// Clone array
let copy = [...arr];
// Combine objects
let obj = { ...obj1, ...obj2 };
// Clean and readable!
// Pass array to function
Math.max(...numbers);
// Much cleaner!Rest Parameter (...)
The rest parameter collects multiple arguments into an array. Same syntax, opposite purpose!
1. Function Rest Parameters
// Rest parameter collects all arguments into array
function sum(...numbers) {
console.log(numbers); // Array of all arguments
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum(10, 20, 30, 40, 50, 60)); // 210
// Old way: arguments object (not an array!)
function oldSum() {
console.log(arguments); // Array-like, not real array
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
// Rest is better: real array with array methods
function average(...numbers) {
if (numbers.length === 0) return 0;
let sum = numbers.reduce((a, b) => a + b);
return sum / numbers.length;
}
console.log(average(10, 20, 30)); // 202. Rest with Named Parameters
// Combine named parameters with rest (rest must be last!)
function createUser(name, age, ...interests) {
return {
name,
age,
interests // Array of remaining arguments
};
}
let user = createUser('Alice', 25, 'coding', 'reading', 'hiking');
console.log(user);
// {
// name: 'Alice',
// age: 25,
// interests: ['coding', 'reading', 'hiking']
// }
// Practical: Logger with category
function log(level, message, ...details) {
console.log(`[${level}] ${message}`);
if (details.length > 0) {
console.log('Details:', details);
}
}
log('INFO', 'User logged in');
log('ERROR', 'Database connection failed', 'timeout', 'host: db.example.com');
// [ERROR] Database connection failed
// Details: ['timeout', 'host: db.example.com']
// Must be last parameter
// function bad(name, ...rest, age) {} // Error!
function good(name, age, ...rest) {} // OK3. Rest in Destructuring
// Array destructuring with rest
let [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
// Skip elements
let [a, , ...others] = [10, 20, 30, 40];
console.log(a); // 10
console.log(others); // [30, 40]
// Object destructuring with rest
let user = {
name: 'Alice',
age: 25,
email: 'alice@test.com',
city: 'NYC',
country: 'USA'
};
let { name, age, ...otherInfo } = user;
console.log(name); // "Alice"
console.log(age); // 25
console.log(otherInfo); // { email: '...', city: 'NYC', country: 'USA' }
// Practical: Extract some props, keep others
function UserCard({ name, avatar, ...props }) {
console.log('Display:', name, avatar);
console.log('Pass to parent:', props);
}
UserCard({
name: 'Alice',
avatar: '/avatar.png',
onClick: () => {},
className: 'card',
dataId: 123
});
// Display: Alice /avatar.png
// Pass to parent: { onClick: [Function], className: 'card', dataId: 123 }Practical Patterns
Pattern 1: Merging Arrays
// Add elements to array immutably
let todos = ['Task 1', 'Task 2'];
// Add to end
let withNewTodo = [...todos, 'Task 3'];
console.log(withNewTodo); // ['Task 1', 'Task 2', 'Task 3']
// Add to beginning
let withUrgent = ['URGENT', ...todos];
console.log(withUrgent); // ['URGENT', 'Task 1', 'Task 2']
// Insert in middle
let index = 1;
let withInserted = [
...todos.slice(0, index),
'Inserted Task',
...todos.slice(index)
];
console.log(withInserted); // ['Task 1', 'Inserted Task', 'Task 2']
// Remove element immutably
let toRemove = 'Task 1';
let filtered = todos.filter(t => t !== toRemove);
// Or with spread and slice:
let removed = [...todos.slice(0, 0), ...todos.slice(1)];Pattern 2: Merging Objects (Config/Settings)
// Default settings pattern
function createConfig(userConfig = {}) {
let defaults = {
theme: 'light',
fontSize: 14,
animations: true,
notifications: true,
language: 'en'
};
// User config overrides defaults
return { ...defaults, ...userConfig };
}
let config1 = createConfig();
console.log(config1); // All defaults
let config2 = createConfig({ theme: 'dark', fontSize: 16 });
console.log(config2);
// { theme: 'dark', fontSize: 16, animations: true, ... }
// API request options
function fetchData(url, customOptions = {}) {
let defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
timeout: 5000
};
let options = { ...defaultOptions, ...customOptions };
return fetch(url, options);
}
fetchData('/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'Alice' })
});Pattern 3: Removing Properties
// Remove properties using destructuring + rest
let user = {
id: 1,
name: 'Alice',
password: 'secret123',
email: 'alice@test.com',
role: 'admin'
};
// Remove password (don't send to client)
let { password, ...safeUser } = user;
console.log(safeUser);
// { id: 1, name: 'Alice', email: 'alice@test.com', role: 'admin' }
// Remove multiple properties
let { password: p, email: e, ...publicData } = user;
console.log(publicData);
// { id: 1, name: 'Alice', role: 'admin' }
// Function to remove specific keys
function omit(obj, ...keys) {
let copy = { ...obj };
keys.forEach(key => delete copy[key]);
return copy;
}
let cleaned = omit(user, 'password', 'email');
console.log(cleaned);
// { id: 1, name: 'Alice', role: 'admin' }Pattern 4: Function Argument Flexibility
// Accept variable number of arguments
function multiply(multiplier, ...numbers) {
return numbers.map(n => n * multiplier);
}
console.log(multiply(2, 1, 2, 3)); // [2, 4, 6]
console.log(multiply(10, 5, 10, 15)); // [50, 100, 150]
// Build query strings
function buildURL(base, ...params) {
let query = params
.map(([key, value]) => `${key}=${value}`)
.join('&');
return `${base}?${query}`;
}
let url = buildURL('/api/users', ['page', 1], ['limit', 10], ['sort', 'name']);
console.log(url); // "/api/users?page=1&limit=10&sort=name"
// Variadic math functions
function min(...numbers) {
return numbers.reduce((min, n) => n < min ? n : min);
}
function max(...numbers) {
return numbers.reduce((max, n) => n > max ? n : max);
}
console.log(min(5, 2, 8, 1, 9)); // 1
console.log(max(5, 2, 8, 1, 9)); // 9Practical Examples
Example 1: Shopping Cart Operations
class ShoppingCart {
constructor() {
this.items = [];
}
// Add items (using spread)
addItems(...newItems) {
this.items = [...this.items, ...newItems];
}
// Remove item immutably
removeItem(itemId) {
this.items = this.items.filter(item => item.id !== itemId);
}
// Update item quantity
updateQuantity(itemId, quantity) {
this.items = this.items.map(item =>
item.id === itemId
? { ...item, quantity } // Spread + override
: item
);
}
// Merge carts
mergeCarts(...otherCarts) {
let allItems = otherCarts.reduce(
(items, cart) => [...items, ...cart.items],
[...this.items]
);
this.items = allItems;
}
// Get total
getTotal() {
return this.items.reduce(
(total, item) => total + (item.price * item.quantity),
0
);
}
}
let cart = new ShoppingCart();
cart.addItems(
{ id: 1, name: 'Book', price: 20, quantity: 1 },
{ id: 2, name: 'Pen', price: 2, quantity: 5 }
);
console.log(cart.items.length); // 2
cart.updateQuantity(1, 2); // Change book quantity to 2
console.log(cart.getTotal()); // 50 (20*2 + 2*5)Example 2: Component Props Pattern
// React-style component with rest/spread
function Button({ type = 'button', children, onClick, ...restProps }) {
// Extract specific props, pass rest to DOM element
return {
element: 'button',
type,
onClick,
children,
// Spread rest props (className, id, data-*, etc.)
...restProps
};
}
let button = Button({
type: 'submit',
children: 'Submit',
onClick: () => console.log('Clicked'),
className: 'btn btn-primary',
id: 'submit-btn',
'data-testid': 'submit',
disabled: false
});
console.log(button);
// {
// element: 'button',
// type: 'submit',
// onClick: [Function],
// children: 'Submit',
// className: 'btn btn-primary',
// id: 'submit-btn',
// 'data-testid': 'submit',
// disabled: false
// }
// HOC pattern: Add props
function withLogging(Component) {
return function LoggedComponent(props) {
console.log('Props:', props);
return Component(props);
};
}
// Wrapper adds props
function withDefaults(Component, defaults) {
return function WithDefaultsComponent(props) {
let mergedProps = { ...defaults, ...props };
return Component(mergedProps);
};
}
let EnhancedButton = withDefaults(Button, {
className: 'btn',
type: 'button'
});Example 3: State Management
// Immutable state updates with spread
class Store {
constructor(initialState = {}) {
this.state = initialState;
}
// Update nested state immutably
updateUser(updates) {
this.state = {
...this.state,
user: {
...this.state.user,
...updates
}
};
}
// Add to array
addTodo(todo) {
this.state = {
...this.state,
todos: [...this.state.todos, todo]
};
}
// Remove from array
removeTodo(todoId) {
this.state = {
...this.state,
todos: this.state.todos.filter(t => t.id !== todoId)
};
}
// Update nested array item
updateTodo(todoId, updates) {
this.state = {
...this.state,
todos: this.state.todos.map(todo =>
todo.id === todoId
? { ...todo, ...updates }
: todo
)
};
}
// Merge multiple updates
setState(...updates) {
this.state = Object.assign({}, this.state, ...updates);
}
}
let store = new Store({
user: { name: 'Alice', age: 25 },
todos: []
});
store.updateUser({ age: 26, email: 'alice@test.com' });
console.log(store.state.user);
// { name: 'Alice', age: 26, email: 'alice@test.com' }
store.addTodo({ id: 1, text: 'Learn JavaScript', done: false });
store.addTodo({ id: 2, text: 'Build project', done: false });
store.updateTodo(1, { done: true });
console.log(store.state.todos);
// [
// { id: 1, text: 'Learn JavaScript', done: true },
// { id: 2, text: 'Build project', done: false }
// ]Rest and Spread Operators
Master ... for collecting and spreading
console.log() to see your output in the console above.Key Takeaways
...has two uses: spread and rest (opposite purposes)- Spread: expands arrays/objects into individual elements
- Use spread to clone, combine, and manipulate arrays/objects
[...arr]clones array,{...obj}clones object (shallow)- Rest: collects multiple values into an array
- Use rest in function parameters:
function(...args) - Use rest in destructuring:
let [a, ...rest] = arr - Rest parameter must be last in function parameters
- Perfect for immutable updates in state management
- Essential for modern JavaScript patterns
What's Next?
Congratulations! You've completed the Modern JavaScript Features section and now understand rest and spread operators—the versatile ... syntax for collecting and spreading values. You've mastered both uses and know when to apply each!
You've now completed 53 out of 62 JavaScript tutorial pages (85% complete)! You've finished 12 major sections and are in the home stretch. Outstanding progress!
💪 Practice Challenge:
Before moving on, try:
- Clone arrays and objects using spread
- Combine multiple arrays/objects with spread
- Create a function that accepts variable arguments with rest
- Use rest in destructuring to extract remaining properties
- Build an immutable state update function using spread