The spread operator (...) is a powerful ES6 feature for working with objects. It provides a clean syntax for copying and merging objects, making your code more concise and readable. However, understanding the difference between shallow and deep copying is crucial to avoid bugs with nested objects. Let's master object cloning and the spread operator!
Understanding Object References
Before learning about cloning, it's important to understand how object references work in JavaScript.
// Objects are stored by reference
let user1 = {
name: "Alice",
age: 25
};
// Assignment creates a reference, NOT a copy
let user2 = user1;
console.log(user1); // { name: "Alice", age: 25 }
console.log(user2); // { name: "Alice", age: 25 }
// Modify user2
user2.age = 30;
// Both are affected (same object!)
console.log(user1.age); // 30
console.log(user2.age); // 30
// They reference the same object
console.log(user1 === user2); // true
// Primitive values work differently
let a = 5;
let b = a; // Copy the value
b = 10;
console.log(a); // 5 (unchanged)
console.log(b); // 10Key Concept:
When you assign an object to a variable, you're storing a reference to the object, not the object itself. This means multiple variables can point to the same object in memory.
Spread Operator for Objects
The spread operator (...) creates a shallow copy of an object by spreading its properties into a new object.
Basic Copying
let original = {
name: "Alice",
age: 25,
city: "NYC"
};
// Create a copy using spread operator
let copy = { ...original };
console.log(original); // { name: "Alice", age: 25, city: "NYC" }
console.log(copy); // { name: "Alice", age: 25, city: "NYC" }
// They are different objects
console.log(original === copy); // false
// Modify copy
copy.age = 30;
console.log(original.age); // 25 (unchanged!)
console.log(copy.age); // 30
// Original is not affected
console.log(original); // { name: "Alice", age: 25, city: "NYC" }
console.log(copy); // { name: "Alice", age: 30, city: "NYC" }Spread creates an independent copy
Reference (Not a Copy)
// Without spread operator
let original = {
name: "Alice",
age: 25
};
// Reference, NOT copy
let copy = original;
copy.age = 30;
console.log(original.age); // 30 (affected!)
console.log(copy.age); // 30
// Same object
console.log(original === copy); // trueSpread Operator (Real Copy)
// With spread operator
let original = {
name: "Alice",
age: 25
};
// Create actual copy
let copy = { ...original };
copy.age = 30;
console.log(original.age); // 25 (unchanged)
console.log(copy.age); // 30
// Different objects
console.log(original === copy); // falseMerging Objects
The spread operator is perfect for merging multiple objects together.
Basic Merging
let defaults = {
theme: "light",
notifications: true,
language: "en"
};
let userPreferences = {
theme: "dark",
fontSize: 14
};
// Merge objects (later properties override earlier ones)
let settings = { ...defaults, ...userPreferences };
console.log(settings);
// {
// theme: "dark", // Overridden by userPreferences
// notifications: true, // From defaults
// language: "en", // From defaults
// fontSize: 14 // From userPreferences
// }
// Order matters!
let settings2 = { ...userPreferences, ...defaults };
console.log(settings2);
// {
// theme: "light", // defaults override userPreferences
// fontSize: 14,
// notifications: true,
// language: "en"
// }Merging Multiple Objects
let obj1 = { a: 1, b: 2 };
let obj2 = { c: 3, d: 4 };
let obj3 = { e: 5, f: 6 };
// Merge all three
let merged = { ...obj1, ...obj2, ...obj3 };
console.log(merged);
// { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }
// Add new properties while merging
let extended = {
...obj1,
...obj2,
g: 7,
h: 8
};
console.log(extended);
// { a: 1, b: 2, c: 3, d: 4, g: 7, h: 8 }Overriding Properties
let user = {
name: "Alice",
age: 25,
email: "alice@test.com"
};
// Update specific properties
let updatedUser = {
...user,
age: 26, // Override age
city: "NYC" // Add new property
};
console.log(updatedUser);
// {
// name: "Alice",
// age: 26, // Updated
// email: "alice@test.com",
// city: "NYC" // Added
// }
// Original unchanged
console.log(user);
// { name: "Alice", age: 25, email: "alice@test.com" }
// Practical: Add timestamp to object
let data = { value: 100, type: "payment" };
let withTimestamp = {
...data,
timestamp: new Date().toISOString()
};
console.log(withTimestamp);Shallow Copy vs Deep Copy
The spread operator creates a shallow copy—nested objects are still referenced, not copied.
The Problem with Shallow Copies
let user = {
name: "Alice",
age: 25,
address: {
city: "NYC",
country: "USA"
}
};
// Shallow copy
let copy = { ...user };
// Modify top-level property (works as expected)
copy.name = "Bob";
console.log(user.name); // "Alice" (unchanged)
console.log(copy.name); // "Bob"
// Modify nested object (problem!)
copy.address.city = "LA";
console.log(user.address.city); // "LA" (changed!)
console.log(copy.address.city); // "LA"
// Why? Nested objects are referenced, not copied
console.log(user.address === copy.address); // true (same object!)
// Visual representation:
// user.address ──┐
// ├──> { city: "LA", country: "USA" }
// copy.address ──┘Warning:
Shallow copies only copy the first level of properties. Nested objects, arrays, and other reference types are still shared between the original and the copy. This can lead to unexpected bugs!
Deep Copying Methods
let user = {
name: "Alice",
age: 25,
address: {
city: "NYC",
country: "USA"
},
hobbies: ["reading", "coding"]
};
// Method 1: JSON parse/stringify (simple but limited)
let deepCopy1 = JSON.parse(JSON.stringify(user));
deepCopy1.address.city = "LA";
deepCopy1.hobbies.push("gaming");
console.log(user.address.city); // "NYC" (unchanged!)
console.log(user.hobbies.length); // 2 (unchanged!)
// Limitations of JSON method:
// - Loses functions
// - Loses undefined values
// - Loses Dates (converts to strings)
// - Can't handle circular references
let withFunction = {
name: "Alice",
greet: function() { return "Hello"; }
};
let jsonCopy = JSON.parse(JSON.stringify(withFunction));
console.log(jsonCopy); // { name: "Alice" } - greet is lost!
// Method 2: structuredClone() (modern, recommended)
let user2 = {
name: "Bob",
address: {
city: "NYC"
}
};
let deepCopy2 = structuredClone(user2);
deepCopy2.address.city = "LA";
console.log(user2.address.city); // "NYC" (unchanged!)
// structuredClone handles more types correctly
// But still can't clone functions
// Method 3: Manual deep copy function
function deepClone(obj) {
// Handle null and primitives
if (obj === null || typeof obj !== "object") {
return obj;
}
// Handle arrays
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
// Handle objects
let cloned = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
let user3 = {
name: "Charlie",
address: { city: "NYC" },
hobbies: ["reading"]
};
let deepCopy3 = deepClone(user3);
deepCopy3.address.city = "LA";
deepCopy3.hobbies.push("gaming");
console.log(user3.address.city); // "NYC"
console.log(user3.hobbies.length); // 1Object.assign() Alternative
Before the spread operator, Object.assign() was the standard way to copy and merge objects. It works similarly but with different syntax.
let original = {
name: "Alice",
age: 25
};
// Using Object.assign() - old way
let copy1 = Object.assign({}, original);
// Using spread operator - modern way (preferred)
let copy2 = { ...original };
console.log(copy1); // { name: "Alice", age: 25 }
console.log(copy2); // { name: "Alice", age: 25 }
// Merging with Object.assign()
let defaults = { theme: "light", size: "medium" };
let custom = { theme: "dark" };
let merged1 = Object.assign({}, defaults, custom);
// { theme: "dark", size: "medium" }
// Same with spread (cleaner)
let merged2 = { ...defaults, ...custom };
// { theme: "dark", size: "medium" }
// Object.assign modifies first argument
let target = { a: 1 };
Object.assign(target, { b: 2 });
console.log(target); // { a: 1, b: 2 } (modified!)
// Spread always creates new object
let obj = { a: 1 };
let newObj = { ...obj, b: 2 };
console.log(obj); // { a: 1 } (unchanged)
console.log(newObj); // { a: 1, b: 2 }Best Practice: Use the spread operator over Object.assign() for cleaner, more readable code. The spread operator is the modern standard.
Practical Patterns
Pattern 1: Updating State Immutably
// Immutable updates (used in React, Redux, etc.)
let state = {
user: {
name: "Alice",
age: 25
},
settings: {
theme: "light"
}
};
// Update nested property immutably
let newState = {
...state,
user: {
...state.user,
age: 26 // Update age
}
};
console.log(state.user.age); // 25 (original unchanged)
console.log(newState.user.age); // 26
// Add property to nested object
let stateWithCity = {
...state,
user: {
...state.user,
city: "NYC"
}
};
console.log(stateWithCity);
// {
// user: { name: "Alice", age: 25, city: "NYC" },
// settings: { theme: "light" }
// }Pattern 2: Conditional Properties
function createUser(name, email, isAdmin) {
return {
name,
email,
createdAt: new Date(),
// Conditionally add admin property
...(isAdmin && { role: "admin", permissions: ["read", "write", "delete"] })
};
}
let regularUser = createUser("Alice", "alice@test.com", false);
console.log(regularUser);
// { name: "Alice", email: "alice@test.com", createdAt: ... }
let adminUser = createUser("Bob", "bob@test.com", true);
console.log(adminUser);
// {
// name: "Bob",
// email: "bob@test.com",
// createdAt: ...,
// role: "admin",
// permissions: ["read", "write", "delete"]
// }
// Multiple conditional properties
function createProduct(name, price, options = {}) {
return {
name,
price,
...(options.featured && { featured: true }),
...(options.discount && { discount: options.discount }),
...(options.tags && { tags: options.tags })
};
}
let product1 = createProduct("Laptop", 1000);
console.log(product1); // { name: "Laptop", price: 1000 }
let product2 = createProduct("Mouse", 25, {
featured: true,
discount: 10,
tags: ["electronics", "accessories"]
});
console.log(product2);
// {
// name: "Mouse",
// price: 25,
// featured: true,
// discount: 10,
// tags: ["electronics", "accessories"]
// }Pattern 3: Removing Properties
let user = {
name: "Alice",
age: 25,
email: "alice@test.com",
password: "secret123",
role: "admin"
};
// Remove password using destructuring and rest
let { password, ...safeUser } = user;
console.log(safeUser);
// { name: "Alice", age: 25, email: "alice@test.com", role: "admin" }
// Remove multiple properties
let { password: pwd, role, ...publicUser } = user;
console.log(publicUser);
// { name: "Alice", age: 25, email: "alice@test.com" }
// Function to remove sensitive data
function sanitizeUser(user) {
let { password, privateKey, ...safe } = user;
return safe;
}
let rawUser = {
name: "Bob",
email: "bob@test.com",
password: "secret",
privateKey: "key123"
};
console.log(sanitizeUser(rawUser));
// { name: "Bob", email: "bob@test.com" }Pattern 4: Default Options with Override
function fetchData(url, options = {}) {
// Default options
let defaultOptions = {
method: "GET",
headers: {
"Content-Type": "application/json"
},
timeout: 5000
};
// Merge with user options (user options override defaults)
let finalOptions = {
...defaultOptions,
...options,
// Deep merge headers
headers: {
...defaultOptions.headers,
...options.headers
}
};
console.log(`Fetching ${url} with:`, finalOptions);
return finalOptions;
}
// Use defaults
fetchData("/api/users");
// { method: "GET", headers: { "Content-Type": "application/json" }, timeout: 5000 }
// Override some options
fetchData("/api/data", {
method: "POST",
timeout: 10000,
headers: {
"Authorization": "Bearer token123"
}
});
// {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// "Authorization": "Bearer token123"
// },
// timeout: 10000
// }Practical Examples
Example 1: Configuration Manager
class ConfigManager {
constructor(defaults = {}) {
this.defaults = defaults;
this.config = { ...defaults };
}
set(options) {
// Merge with existing config
this.config = {
...this.config,
...options
};
}
get(key) {
return this.config[key];
}
reset() {
// Reset to defaults
this.config = { ...this.defaults };
}
getAll() {
// Return copy of config
return { ...this.config };
}
}
let config = new ConfigManager({
theme: "light",
language: "en",
notifications: true
});
console.log("Initial:", config.getAll());
config.set({ theme: "dark", fontSize: 14 });
console.log("Updated:", config.getAll());
config.reset();
console.log("Reset:", config.getAll());Example 2: Form Data Handler
function processFormData(formData, validations = {}) {
// Default form structure
let defaultForm = {
submitted: false,
errors: {},
touched: {},
values: {}
};
// Merge with provided data
let form = {
...defaultForm,
...formData,
// Deep merge values
values: {
...defaultForm.values,
...formData.values
}
};
// Apply validations
Object.keys(validations).forEach(field => {
if (validations[field](form.values[field]) === false) {
form.errors[field] = `Invalid ${field}`;
}
});
return form;
}
let formData = {
values: {
email: "alice@test.com",
age: 25
},
touched: {
email: true
}
};
let validations = {
email: value => value && value.includes("@"),
age: value => value >= 18
};
let result = processFormData(formData, validations);
console.log(result);Spread Operator and Cloning Practice
Experiment with object copying and merging
console.log() to see your output in the console above.Key Takeaways
- Use spread operator
{ ...obj }to copy objects - Spread creates shallow copies—nested objects are referenced
- Merge objects with
{ ...obj1, ...obj2 } - Later properties override earlier ones in merges
- Use
structuredClone()or JSON methods for deep copies - Assignment (
=) creates references, not copies - Spread operator is preferred over Object.assign()
- Use destructuring with rest to remove properties
- Spread enables immutable updates (important for React/Redux)
- Always copy objects when you don't want to modify the original
What's Next?
Congratulations! You've completed the Objects section and now have a comprehensive understanding of JavaScript objects—from basic properties to advanced cloning and merging techniques. Objects are fundamental to JavaScript, and you now have the skills to work with them effectively!
You've now completed 36 out of 62 JavaScript tutorial pages, covering Introduction, Variables & Data Types, Operators, Control Flow, Loops, Functions, Arrays, and Objects. You're making excellent progress through this comprehensive JavaScript course!
💪 Practice Challenge:
Before moving on, try creating:
- A function that deep clones an object with nested properties
- Merge multiple configuration objects with proper precedence
- Update a nested object property immutably (without mutating original)
- Create a function that removes specified properties from an object
- Build a state management system that uses immutable updates