Working with Nested Data

JavaScript ES2024 · ✓ verified by execution on 2026-08-15

Real-world data is rarely flat. It almost always arrives as complex, nested structures. Whether you are dealing with a response from a web API, reading a JSON configuration file, or structuring the state of an application, you will encounter data that is several levels deep. An object is a collection of properties, and those properties can themselves be objects or arrays.

In this lesson, we will explore how to access, traverse, and reshape nested data, as well as the common pitfalls you will encounter along the way.

Reading and Writing Nested Objects

You can access properties deep inside an object by chaining dot notation together. The engine will evaluate the chain from left to right, reaching deeper into the object with every dot.

javascript
const user = { name: 'Alice', address: { city: 'Tokyo', zip: '100-0001' } };
console.log(user.address.city);
Output
Tokyo

Modifying a nested property works the exact same way. You simply assign a new value to the deep path. It’s important to remember that this alters the original object in place.

javascript
const user = { name: 'Bob', settings: { theme: 'light' } };
user.settings.theme = 'dark';
console.log(user.settings.theme);
Output
dark

You can also use destructuring to unpack deeply nested properties in a single line. A property can be unpacked from an object and assigned to a variable with a different name, which is exceptionally useful when pulling specific data out of a massive API response.

javascript
const user = { name: 'Alice', contact: { email: '[email protected]' } };
const { contact: { email } } = user;
console.log(email);

Arrays of Objects

Arrays can contain objects, forming an array of objects. This is the shape of essentially all real data fetched from a database. Instead of a single user, you usually have a list of hundreds.

javascript
const users = [
  { name: 'Alice', role: 'admin' },
  { name: 'Bob', role: 'user' }
];
console.log(users[0].name);
console.log(users[1].role);
Output
Alice
user

When working with arrays of objects, you will frequently use array methods like map(). The map() method creates a new array populated with the results of calling a provided function on every element in the calling array. This makes it trivial to extract a specific property from an array of objects, or reshape the data entirely into a format that a UI component might expect.

javascript
const users = [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }];
const names = users.map(u => u.name);
console.log(names);
Output
[ 'Alice', 'Bob', 'Charlie' ]

The Reference Trap

One of the most notorious pitfalls in JavaScript involves how it handles object memory. Two distinct objects are never equal, even if they have the exact same properties. When you copy an object or an array, you must be careful: JavaScript uses references for objects.

Why does the language do this? Because objects can be massive. If JavaScript passed objects by value (creating a full clone every time you assigned a variable), the language would grind to a halt under the memory and CPU overhead. Instead, it just hands over a tiny pointer to the same underlying memory address.

This creates the shallow copy trap.

Predict the output javascript

Read the code. What exactly will it print? Commit to an answer before you look.

const original = { counts: [10, 20] };
const clone = { ...original };
clone.counts.push(30);
console.log(original.counts.length);
Output
3

Because the spread operator (...) only performs a shallow copy, the top-level object is duplicated, but the nested counts array is not. Both original.counts and clone.counts point to the exact same array in memory. When you mutate the array through the clone variable, the original variable instantly reflects the change because they are the same array.

If you truly need a completely isolated duplicate, you must perform a deep clone (e.g., using structuredClone()), which travels all the way down the nested tree and duplicates every inner object and array it encounters.

javascript · visualize
const a = { address: { city: 'Paris' } };
const b = { ...a };

Memory Heap { city: 'Paris' } Variable a Variable b {address:} {address:}
Shallow copies create a new outer container but share the nested object references.

Check yourself

Does assigning an array of objects to a new variable create a full independent copy?

Reveal answer

No, it creates a shallow copy where the nested objects are still shared references. — Assigning or spreading an array of objects only creates a shallow copy. The outer array is new, but the nested objects inside it remain shared references.

What happens if you try to access a nested property on a parent object that is undefined?

Reveal answer

It throws a TypeError. — Accessing a property on undefined throws a TypeError, which is why you must check for existence or use optional chaining (?.).

Does the Array map() method automatically clone nested objects?

Reveal answer

No, the returned array contains references to the original objects unless you explicitly clone them inside the map callback. — The map() method creates a new array, but if you return the original objects, they remain references. You must explicitly create new objects if you want a deep clone.

Is dot notation the only way to access nested properties?

Reveal answer

No, bracket notation works too, and is required for dynamic keys or keys with spaces. — Bracket notation works perfectly fine for nested properties, such as user["address"]["zip code"].

Challenges

Challenge 1 +10 XP

Given an array of `users`, use `map()` to return a new array containing just their email addresses from the nested `contact` object.

javascript
Need a hint? (−25% XP)

Use `users.map(user => user.contact.email)`

Show solution (0 XP)
import fs from 'node:fs';
const users = JSON.parse(fs.readFileSync(0, 'utf-8'));

function getEmails(users) {
  return users.map(user => user.contact.email);
}

console.log(JSON.stringify(getEmails(users)));

🐞 Bug Hunt +15 XP

Write a function `updateHighScore(player, newScore)` that updates `player.stats.highScore` only if `newScore` is greater than the current high score. Return the updated player object.

This code runs. It just does the wrong thing. Read it, find the defect, fix it — the tests below decide when you are right.

javascript
  • Test 1 (input: "[{\"name\":\"Alice\",\"stats\":{\"highScore\":100}}, 150]") — expects "{\"name\":\"Alice\",\"stats\":{\"highScore\":150}}"
  • Test 2 (input: "[{\"name\":\"Bob\",\"stats\":{\"highScore\":200}}, 150]") — expects "{\"name\":\"Bob\",\"stats\":{\"highScore\":200}}"
Need a hint? (−25% XP)

Check `if (newScore > player.stats.highScore)` before assigning.

Show solution (0 XP)
import fs from 'node:fs';
const [player, newScore] = JSON.parse(fs.readFileSync(0, 'utf-8'));

function updateHighScore(player, newScore) {
  if (newScore > player.stats.highScore) {
    player.stats.highScore = newScore;
  }
  return player;
}

console.log(JSON.stringify(updateHighScore(player, newScore)));