JavaScript Spread Operator (...): Complete Guide
Master the JavaScript spread operator for arrays, objects, and function arguments. Learn practical uses, performance tips, and common patterns.
JavaScript Spread Operator (...): Complete Guide
The spread operator (...
) is one of the most versatile features in modern JavaScript. It allows you to expand iterable elements and create more concise, readable code.
What is the Spread Operator?
The spread operator (...
) expands iterable objects into individual elements. It works with arrays, objects, strings, and other iterables.
// Array spread
const numbers = [1, 2, 3];
console.log(...numbers); // 1 2 3
// Object spread
const person = { name: 'John', age: 30 };
const clone = { ...person };
// String spread
const str = 'Hello';
console.log([...str]); // ['H', 'e', 'l', 'l', 'o']
Array Spread
Copying Arrays
// Shallow copy array
const original = [1, 2, 3, 4, 5];
const copy = [...original];
copy.push(6);
console.log(original); // [1, 2, 3, 4, 5]
console.log(copy); // [1, 2, 3, 4, 5, 6]
// Compare with traditional methods
const copy1 = original.slice();
const copy2 = Array.from(original);
const copy3 = [...original]; // Most concise
// Copying nested arrays (still shallow)
const nested = [
[1, 2],
[3, 4],
];
const shallowCopy = [...nested];
shallowCopy[0].push(3);
console.log(nested[0]); // [1, 2, 3] - original affected!
Combining Arrays
// Concatenate arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// Insert in the middle
const middle = [...arr1, 'middle', ...arr2];
console.log(middle); // [1, 2, 3, 'middle', 4, 5, 6]
// Multiple arrays
const arr3 = [7, 8, 9];
const all = [...arr1, ...arr2, ...arr3];
// Conditional spreading
const includeExtra = true;
const result = [...arr1, ...(includeExtra ? arr2 : []), ...arr3];
Array Manipulation
// Add elements to beginning/end
const numbers = [2, 3, 4];
const expanded = [1, ...numbers, 5];
console.log(expanded); // [1, 2, 3, 4, 5]
// Remove duplicates
const duplicates = [1, 2, 2, 3, 3, 3, 4];
const unique = [...new Set(duplicates)];
console.log(unique); // [1, 2, 3, 4]
// Convert NodeList to Array
const divs = document.querySelectorAll('div');
const divArray = [...divs];
// Convert arguments to array
function sum() {
const args = [...arguments];
return args.reduce((a, b) => a + b, 0);
}
Function Arguments
Spread in Function Calls
// Pass array as individual arguments
const numbers = [5, 6, 2, 3, 7];
const max = Math.max(...numbers);
console.log(max); // 7
// Traditional way
const max2 = Math.max.apply(null, numbers);
// Multiple arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
console.log(Math.max(...arr1, ...arr2)); // 6
// Practical examples
function greet(first, last, ...titles) {
console.log(`Hello ${titles.join(' ')} ${first} ${last}`);
}
const nameParts = ['John', 'Doe'];
const titles = ['Dr.', 'Prof.'];
greet(...nameParts, ...titles);
Rest Parameters vs Spread
// Rest parameters (gathering)
function multiply(multiplier, ...numbers) {
return numbers.map((n) => n * multiplier);
}
console.log(multiply(2, 1, 2, 3)); // [2, 4, 6]
// Spread (expanding)
const nums = [1, 2, 3];
console.log(multiply(2, ...nums)); // [2, 4, 6]
// Both together
function process(first, ...rest) {
console.log('First:', first);
console.log('Rest:', rest);
// Spread the rest
return [first * 2, ...rest.map((x) => x * 2)];
}
console.log(process(...[1, 2, 3, 4])); // [2, 4, 6, 8]
Object Spread
Copying Objects
// Shallow copy object
const original = {
name: 'John',
age: 30,
city: 'New York',
};
const copy = { ...original };
copy.age = 31;
console.log(original.age); // 30
console.log(copy.age); // 31
// Nested objects (shallow copy issue)
const user = {
name: 'John',
preferences: {
theme: 'dark',
language: 'en',
},
};
const userCopy = { ...user };
userCopy.preferences.theme = 'light';
console.log(user.preferences.theme); // 'light' - original affected!
// Deep copy solution
const deepCopy = {
...user,
preferences: { ...user.preferences },
};
Merging Objects
// Merge objects
const defaults = {
theme: 'light',
fontSize: 16,
showSidebar: true,
};
const userPreferences = {
theme: 'dark',
fontSize: 18,
};
const settings = { ...defaults, ...userPreferences };
console.log(settings);
// { theme: 'dark', fontSize: 18, showSidebar: true }
// Order matters - last one wins
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 3, c: 4 }
// Multiple objects
const config = {
...defaults,
...userPreferences,
...sessionOverrides,
version: '2.0', // Additional properties
};
Object Property Manipulation
// Add properties
const person = { name: 'John', age: 30 };
const employee = {
...person,
id: 123,
department: 'IT',
};
// Update properties
const updated = {
...person,
age: 31,
city: 'Boston',
};
// Remove properties (using rest)
const { age, ...personWithoutAge } = person;
console.log(personWithoutAge); // { name: 'John' }
// Conditional properties
const includeEmail = true;
const user = {
name: 'John',
...(includeEmail && { email: 'john@example.com' }),
...(isAdmin && { role: 'admin' }),
};
// Dynamic property names
const key = 'dynamicKey';
const obj = {
...otherObj,
[key]: 'value',
};
Practical Use Cases
State Management
// React-style state updates
const state = {
user: { name: 'John', age: 30 },
posts: [],
loading: false,
};
// Update nested state immutably
const newState = {
...state,
user: {
...state.user,
age: 31,
},
loading: true,
};
// Redux-style reducer
function reducer(state = initialState, action) {
switch (action.type) {
case 'UPDATE_USER':
return {
...state,
user: { ...state.user, ...action.payload },
};
case 'ADD_POST':
return {
...state,
posts: [...state.posts, action.payload],
};
default:
return state;
}
}
API Request Handling
// Flexible API requests
async function apiRequest(endpoint, options = {}) {
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getToken()}`,
},
credentials: 'include',
};
const finalOptions = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...options.headers,
},
};
return fetch(endpoint, finalOptions);
}
// Usage
apiRequest('/users', {
method: 'POST',
body: JSON.stringify({ name: 'John' }),
headers: { 'X-Custom': 'value' },
});
Array Methods Implementation
// Custom array methods using spread
Array.prototype.customPush = function (...elements) {
return [...this, ...elements];
};
Array.prototype.customUnshift = function (...elements) {
return [...elements, ...this];
};
Array.prototype.customSplice = function (start, deleteCount, ...items) {
return [
...this.slice(0, start),
...items,
...this.slice(start + deleteCount),
];
};
// Usage
const arr = [1, 2, 3];
console.log(arr.customPush(4, 5)); // [1, 2, 3, 4, 5]
console.log(arr.customUnshift(0)); // [0, 1, 2, 3]
console.log(arr.customSplice(1, 1, 'a', 'b')); // [1, 'a', 'b', 3]
Advanced Patterns
Function Composition
// Compose functions with spread
const compose =
(...fns) =>
(x) =>
fns.reduceRight((acc, fn) => fn(acc), x);
const pipe =
(...fns) =>
(x) =>
fns.reduce((acc, fn) => fn(acc), x);
// Usage
const add5 = (x) => x + 5;
const multiply2 = (x) => x * 2;
const subtract3 = (x) => x - 3;
const combined = compose(add5, multiply2, subtract3);
console.log(combined(10)); // 19
// Middleware pattern
const middleware = (...middlewares) => {
return (req, res, next) => {
const runMiddleware = (index) => {
if (index >= middlewares.length) {
return next();
}
middlewares[index](req, res, () => runMiddleware(index + 1));
};
runMiddleware(0);
};
};
Immutable Updates
// Deep immutable updates
function updateNested(obj, path, value) {
const keys = path.split('.');
const update = (current, [key, ...rest]) => {
if (rest.length === 0) {
return { ...current, [key]: value };
}
return {
...current,
[key]: update(current[key] || {}, rest),
};
};
return update(obj, keys);
}
// Usage
const state = {
user: {
profile: {
name: 'John',
settings: {
theme: 'light',
},
},
},
};
const newState = updateNested(state, 'user.profile.settings.theme', 'dark');
Array Transformations
// Flatten arrays
const flatten = (arr) =>
arr.reduce(
(flat, item) => [
...flat,
...(Array.isArray(item) ? flatten(item) : [item]),
],
[]
);
// Zip arrays
const zip = (...arrays) => {
const maxLength = Math.max(...arrays.map((arr) => arr.length));
return Array.from({ length: maxLength }, (_, i) =>
arrays.map((arr) => arr[i])
);
};
// Chunk array
const chunk = (arr, size) =>
Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
);
// Usage examples
console.log(flatten([1, [2, [3, [4]]]])); // [1, 2, 3, 4]
console.log(zip([1, 2, 3], ['a', 'b', 'c'])); // [[1,'a'], [2,'b'], [3,'c']]
console.log(chunk([1, 2, 3, 4, 5], 2)); // [[1, 2], [3, 4], [5]]
Performance Considerations
// Spread can be slower for large arrays
const largeArray = Array(1000000).fill(1);
// Slower
console.time('spread');
const copy1 = [...largeArray];
console.timeEnd('spread');
// Faster alternatives for large arrays
console.time('slice');
const copy2 = largeArray.slice();
console.timeEnd('slice');
// Memory considerations
// Each spread creates a new array/object
function inefficient(arr) {
return [...arr].map((x) => x * 2);
}
function efficient(arr) {
return arr.map((x) => x * 2);
}
// Avoid excessive spreading in loops
// Bad
let result = [];
for (let i = 0; i < 1000; i++) {
result = [...result, i]; // Creates new array each time
}
// Good
const result = [];
for (let i = 0; i < 1000; i++) {
result.push(i);
}
Common Pitfalls
Shallow Copy Issues
// Objects with nested structures
const original = {
data: { value: 1 },
items: [1, 2, 3],
};
const copy = { ...original };
copy.data.value = 2;
copy.items.push(4);
console.log(original.data.value); // 2 - affected!
console.log(original.items); // [1, 2, 3, 4] - affected!
// Solution: Deep spread
const deepCopy = {
...original,
data: { ...original.data },
items: [...original.items],
};
Spreading Non-Iterables
// Cannot spread non-iterables
// const spread = [...null]; // TypeError
// const spread = [...undefined]; // TypeError
// const spread = [...123]; // TypeError
// Safe spreading
const safeSpread = [...(array || [])];
const safeObjectSpread = { ...(obj || {}) };
// Check before spreading
function spreadSafely(value) {
if (value == null) return [];
if (typeof value[Symbol.iterator] === 'function') {
return [...value];
}
return [value];
}
Order Dependencies
// Order matters in object spread
const obj = {
...{ a: 1, b: 2 },
...{ b: 3, c: 4 },
b: 5,
};
console.log(obj); // { a: 1, b: 5, c: 4 }
// Method binding issues
const obj = {
value: 42,
getValue() {
return this.value;
},
};
const spread = { ...obj };
const getValue = spread.getValue;
console.log(getValue()); // undefined - lost context
Best Practices
-
Use spread for immutability
// Good - immutable update const newArray = [...oldArray, newItem]; const newObject = { ...oldObject, newProp: value };
-
Avoid deep nesting
// Hard to read const deep = { ...a, b: { ...a.b, c: { ...a.b.c, d: value } } }; // Better - use utility function const deep = updatePath(a, 'b.c.d', value);
-
Consider performance for large data
// For large arrays, traditional methods might be faster const copy = largeArray.slice();
-
Use destructuring with spread
const { unwanted, ...rest } = object; const [first, ...remainder] = array;
Conclusion
The spread operator is a powerful feature that simplifies many common JavaScript patterns:
- Array operations: copying, merging, converting
- Object manipulation: cloning, merging, updating
- Function arguments: flexible parameter passing
- Immutable updates: state management patterns
- Clean syntax: more readable than traditional methods
Key takeaways:
- Creates shallow copies (be careful with nested structures)
- Order matters when spreading objects
- Great for immutable programming patterns
- Essential for modern JavaScript and frameworks
- Consider performance for large datasets
Master the spread operator to write more concise and expressive JavaScript code!