Episode 1 — Fundamentals / 1.23 — Objects
1.23.e — Nested Objects
In one sentence: Real-world data is almost always nested — objects inside objects — and working safely with deep structures requires optional chaining, understanding shallow vs deep copy, and knowing how to update nested data without unintended mutation.
Navigation: ← 1.23 Overview · 1.23.f — Looping Through Keys →
1. Objects inside objects — deep data structures
Most real-world data is not flat. A user has an address; an address has a city and coordinates; coordinates have latitude and longitude:
const user = {
id: 1,
name: "Alice",
address: {
street: "123 Main St",
city: "Portland",
state: "OR",
zip: "97201",
coordinates: {
lat: 45.5152,
lng: -122.6784,
},
},
preferences: {
theme: "dark",
notifications: {
email: true,
sms: false,
push: true,
},
},
tags: ["admin", "beta-tester"],
};
This is three levels deep in some paths (user.preferences.notifications.email). API responses, configuration files, and database documents regularly have this kind of nesting.
2. Accessing nested properties
Use chained dot or bracket notation:
// Direct chained access
console.log(user.address.city); // "Portland"
console.log(user.address.coordinates.lat); // 45.5152
console.log(user.preferences.notifications.email); // true
// Mixed with bracket notation (for dynamic keys)
const field = "city";
console.log(user.address[field]); // "Portland"
// Accessing arrays inside objects
console.log(user.tags[0]); // "admin"
console.log(user.tags.length); // 2
3. The danger — TypeError if intermediate is null/undefined
If any intermediate value in the chain is null or undefined, the next . access throws:
const user = {
name: "Alice",
// address is missing entirely
};
// CRASH:
// console.log(user.address.city);
// TypeError: Cannot read properties of undefined (reading 'city')
// Also crashes if a value is explicitly null:
const user2 = {
name: "Bob",
address: null,
};
// console.log(user2.address.city);
// TypeError: Cannot read properties of null (reading 'city')
This is one of the most common runtime errors in JavaScript.
4. Optional chaining for safe deep access
The ?. operator short-circuits to undefined instead of throwing:
const user = { name: "Alice" };
// Safe — returns undefined instead of crashing
console.log(user.address?.city); // undefined
console.log(user.address?.coordinates?.lat); // undefined
console.log(user.preferences?.notifications?.email); // undefined
Combining with nullish coalescing for defaults
const user = {
name: "Alice",
preferences: {
theme: "dark",
// notifications not set
},
};
const emailNotif = user.preferences?.notifications?.email ?? true;
console.log(emailNotif); // true (default)
const theme = user.preferences?.theme ?? "light";
console.log(theme); // "dark" (actual value)
Safe method calls on nested objects
const app = {
logger: {
info(msg) { console.log(`[INFO] ${msg}`); },
},
};
app.logger?.info("Application started"); // [INFO] Application started
app.analytics?.track("page_view"); // undefined — no crash
5. Deep copying vs shallow copying — the problem
When you copy an object, you need to understand what "copy" means:
const original = {
name: "Alice",
scores: [95, 88, 72],
address: {
city: "Portland",
},
};
// "Copying" the reference — NOT a copy at all
const ref = original;
ref.name = "Bob";
console.log(original.name); // "Bob" — same object!
The real question is: when you create a new object, do nested objects get their own copies or do they share references?
6. Shallow copy — { ...obj } and Object.assign({}, obj)
A shallow copy creates a new top-level object, but nested objects are still shared:
const original = {
name: "Alice",
scores: [95, 88, 72],
address: {
city: "Portland",
state: "OR",
},
};
// Shallow copy with spread
const copy = { ...original };
// Top-level properties are independent
copy.name = "Bob";
console.log(original.name); // "Alice" — not affected
// BUT nested objects are SHARED
copy.address.city = "Seattle";
console.log(original.address.city); // "Seattle" — CHANGED! Both point to same address object
copy.scores.push(100);
console.log(original.scores); // [95, 88, 72, 100] — CHANGED! Both point to same array
Visual explanation
original ──→ { name: "Alice", address: ──→ { city: "Portland" } }
↑
copy ──→ { name: "Bob", address: ─┘ (same reference!)
Object.assign behaves the same way
const copy2 = Object.assign({}, original);
// Same shallow behavior — nested objects are still shared
7. Deep copy — structuredClone(obj) (modern)
structuredClone creates a completely independent copy at all levels:
const original = {
name: "Alice",
scores: [95, 88, 72],
address: {
city: "Portland",
coordinates: { lat: 45.52, lng: -122.68 },
},
createdAt: new Date("2024-01-15"),
};
const deepCopy = structuredClone(original);
// Modifying the deep copy does NOT affect the original
deepCopy.address.city = "Seattle";
deepCopy.scores.push(100);
deepCopy.address.coordinates.lat = 47.61;
console.log(original.address.city); // "Portland" — safe!
console.log(original.scores); // [95, 88, 72] — safe!
console.log(original.address.coordinates.lat); // 45.52 — safe!
What structuredClone handles
| Type | Supported? |
|---|---|
| Plain objects | Yes |
| Arrays | Yes |
Date | Yes |
Map, Set | Yes |
RegExp | Yes |
ArrayBuffer, typed arrays | Yes |
| Functions | No — throws DataCloneError |
| DOM nodes | No |
| Symbols (as property keys) | No |
| Prototype chain | Not preserved (always plain object) |
// structuredClone cannot handle functions
const withMethod = {
name: "Alice",
greet() { return "hi"; },
};
// structuredClone(withMethod); // DataCloneError!
8. Legacy deep copy — JSON.parse(JSON.stringify(obj))
Before structuredClone, the JSON round-trip was the common workaround:
const original = {
name: "Alice",
address: { city: "Portland" },
scores: [95, 88],
};
const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.address.city = "Seattle";
console.log(original.address.city); // "Portland" — deep copy works
Limitations of JSON round-trip
| Value type | What happens |
|---|---|
undefined | Removed from object; becomes null in array |
Date | Converted to string (not restored as Date) |
RegExp | Becomes {} |
Map, Set | Becomes {} |
Infinity, NaN | Become null |
| Functions | Removed |
| Circular references | Throws TypeError |
const problematic = {
date: new Date("2024-01-15"),
pattern: /hello/gi,
value: undefined,
count: NaN,
};
const copy = JSON.parse(JSON.stringify(problematic));
console.log(copy);
// { date: "2024-01-15T00:00:00.000Z", pattern: {}, count: null }
// date is now a string, pattern is empty object, value is gone, NaN is null
Comparison: structuredClone vs JSON round-trip
| Feature | structuredClone | JSON.parse(JSON.stringify) |
|---|---|---|
Date | Preserved as Date | Becomes string |
undefined | Preserved | Removed / null |
Map / Set | Preserved | Lost (becomes {}) |
NaN / Infinity | Preserved | Becomes null |
| Functions | Throws | Silently removed |
| Circular references | Handles them | Throws |
| Performance | Faster (native) | Slower (serialize + parse) |
| Browser support | Modern (2022+) | Universal |
Recommendation: Use structuredClone unless you need to support very old environments.
9. Modifying nested properties without mutation
In React, Redux, and functional programming, you often need to update a nested value while keeping the original intact. This requires spreading at each level:
const state = {
user: {
name: "Alice",
address: {
city: "Portland",
state: "OR",
},
},
settings: {
theme: "dark",
},
};
// Update city without mutating state
const newState = {
...state, // copy top level
user: {
...state.user, // copy user level
address: {
...state.user.address, // copy address level
city: "Seattle", // override city
},
},
};
console.log(state.user.address.city); // "Portland" — unchanged
console.log(newState.user.address.city); // "Seattle" — updated
// Settings is shared (not spread), which is fine if we didn't change it
console.log(state.settings === newState.settings); // true — same reference (efficient)
The spread-at-every-level pattern
To update: state.user.address.city
{ ...state,
user: { ...state.user,
address: { ...state.user.address,
city: "new value"
}
}
}
Each level creates a new object, but sibling properties at each level keep their references — this is structural sharing and is memory-efficient.
Helper function for nested updates
function updateNested(obj, path, value) {
const keys = path.split(".");
if (keys.length === 1) {
return { ...obj, [keys[0]]: value };
}
const [first, ...rest] = keys;
return {
...obj,
[first]: updateNested(obj[first] ?? {}, rest.join("."), value),
};
}
const original = {
user: { profile: { name: "Alice", age: 25 } },
};
const updated = updateNested(original, "user.profile.name", "Bob");
console.log(updated.user.profile.name); // "Bob"
console.log(original.user.profile.name); // "Alice" — unchanged
10. Real examples
User with address and preferences
const user = {
id: 42,
name: "Alice",
email: "alice@example.com",
address: {
street: "123 Main St",
city: "Portland",
state: "OR",
country: "US",
},
preferences: {
theme: "dark",
language: "en",
notifications: {
email: true,
sms: false,
push: true,
frequency: "daily",
},
},
};
// Safe read
const smsEnabled = user.preferences?.notifications?.sms ?? false;
console.log(smsEnabled); // false
// Immutable update — change notification frequency
const updatedUser = {
...user,
preferences: {
...user.preferences,
notifications: {
...user.preferences.notifications,
frequency: "weekly",
},
},
};
console.log(updatedUser.preferences.notifications.frequency); // "weekly"
console.log(user.preferences.notifications.frequency); // "daily"
API response with nested data
const apiResponse = {
status: 200,
data: {
article: {
id: 101,
title: "Understanding Objects",
author: {
id: 5,
name: "Alice",
avatar: "alice.png",
},
comments: [
{
id: 201,
text: "Great article!",
author: { id: 8, name: "Bob" },
replies: [
{ id: 301, text: "Thanks!", author: { id: 5, name: "Alice" } },
],
},
],
metadata: {
views: 1500,
likes: 42,
tags: ["javascript", "objects", "tutorial"],
},
},
},
};
// Safe deep access
const authorName = apiResponse.data?.article?.author?.name ?? "Unknown";
console.log(authorName); // "Alice"
const firstReply = apiResponse.data?.article?.comments?.[0]?.replies?.[0];
console.log(firstReply?.text); // "Thanks!"
const tags = apiResponse.data?.article?.metadata?.tags ?? [];
console.log(tags.join(", ")); // "javascript, objects, tutorial"
// Deep copy for safe manipulation
const responseCopy = structuredClone(apiResponse);
responseCopy.data.article.metadata.views += 1;
console.log(apiResponse.data.article.metadata.views); // 1500 — original untouched
console.log(responseCopy.data.article.metadata.views); // 1501
Key takeaways
- Nested objects are the norm in real-world data — API responses, configs, state trees.
- Accessing a property on
nullorundefinedthrows TypeError — use optional chaining?.to prevent crashes. - Shallow copy (
{ ...obj },Object.assign) only copies the top level; nested objects are shared references. structuredClone(obj)creates a true deep copy — independent at every level. Use it as the modern default.JSON.parse(JSON.stringify(obj))is a legacy deep copy that losesDate,undefined,Map,Set,NaN, and cannot handle circular references.- For immutable nested updates, spread at every level of the path you are changing — sibling references are efficiently shared.
- Consider helper functions or libraries (Immer, Lodash
_.set) for deeply nested immutable updates to avoid boilerplate.
Explain-It Challenge
Explain without notes:
- Why does modifying a shallow copy's nested property also change the original?
- What is the difference between
structuredCloneand theJSON.parse(JSON.stringify)trick? Name two values they handle differently. - Show the spread-at-every-level pattern to update
state.user.address.cityimmutably.
Navigation: ← 1.23 Overview · 1.23.f — Looping Through Keys →