Skip to main content
Adapted, not copied

Original notes for JavaScript360.org — topic order inspired by Eloquent JavaScript (4th ed.) by Marijn Haverbeke. See the full attribution note.

Chapter 4: Data Structures — Objects & Arrays

Estimated time: ~50 minutes · Chapter type: Concept

What you'll learn

  • How to model real-world data with objects and arrays
  • The array methods you'll reach for constantly
  • Mutability — and why const doesn't mean "unchangeable"
  • Destructuring, rest parameters, and JSON

A running example: a sleep-tracking log

Suppose you're building a small habit tracker that records how many hours you slept each night. A single night's entry is naturally an object — a bag of named properties:

const entry = {
date: '2026-09-01',
hoursSlept: 7.5,
mood: 'rested',
};

A week of entries is naturally an array — an ordered list:

const log = [
{ date: '2026-09-01', hoursSlept: 7.5, mood: 'rested' },
{ date: '2026-09-02', hoursSlept: 5.5, mood: 'tired' },
{ date: '2026-09-03', hoursSlept: 8, mood: 'great' },
];

Almost every real dataset you'll deal with is some combination of these two shapes: arrays of objects, objects containing arrays, and so on.

Properties

You read and write an object's properties with either dot notation (when you know the name ahead of time) or bracket notation (when the name is dynamic, e.g. stored in a variable):

entry.mood;               // "rested"
entry['hoursSlept']; // 7.5

const key = 'mood';
entry[key]; // "rested" — dot notation can't do this

entry.mood = 'okay'; // writing a property
entry.tags = ['weekday']; // adding a brand-new property

Accessing a property that doesn't exist gives undefined rather than throwing an error — which is convenient, but means typos in property names fail silently instead of loudly. entry.hoursSleep (missing the t) simply returns undefined with no warning.

Methods

A property whose value happens to be a function is called a method, and it's the standard way to attach behavior to a piece of data:

const logger = {
entries: [],
add(entry) {
this.entries.push(entry); // shorthand method syntax
},
average() {
const total = this.entries.reduce((sum, e) => sum + e.hoursSlept, 0);
return this.entries.length ? total / this.entries.length : 0;
},
};

logger.add({ date: '2026-09-01', hoursSlept: 7.5 });
logger.add({ date: '2026-09-02', hoursSlept: 6 });
logger.average(); // 6.75

Inside a method, this refers to the object the method was called onlogger, in this case. this is determined by how a function is called, not where it's defined, which is a frequent source of bugs when methods are passed around detached from their object (arrow functions, which don't have their own this, sidestep that problem — one reason they're popular for callbacks).

Objects, under the hood

An object is fundamentally just a collection of key/value pairs, where keys are always strings (or Symbols — Chapter 6) even if you write them as numbers or bare words. Two useful built-ins for inspecting an object:

Object.keys(entry);    // ["date", "hoursSlept", "mood"]
Object.values(entry); // ["2026-09-01", 7.5, "rested"]
Object.entries(entry); // [["date", "2026-09-01"], ["hoursSlept", 7.5], ...]

Object.entries pairs nicely with for...of when you need both the key and value:

for (const [key, value] of Object.entries(entry)) {
console.log(`${key}: ${value}`);
}

Mutability

Objects and arrays are mutable — you can change their contents in place — while primitives (numbers, strings, booleans) are not. This has a consequence that surprises a lot of newcomers: const only locks the binding, not the contents.

const scores = [10, 20];
scores.push(30); // fine — the array is mutated, "scores" still points at it
console.log(scores); // [10, 20, 30]

// scores = []; // TypeError — this reassigns the binding, which const forbids

Two objects with identical contents are still different objects, and === compares object identity, not deep equality:

{ a: 1 } === { a: 1 }; // false — two distinct objects, even though they "look" the same
const x = { a: 1 };
const y = x;
x === y; // true — y is the *same* object as x, not a copy
y.a = 2;
x.a; // 2 — mutating y also affects x, because they're the same object

When you want an independent copy instead of a shared reference, use structuredClone(obj) for a deep copy, or {...obj} / [...arr] (the spread operator) for a shallow one.

Array loops and essential array methods

You already know for...of for a plain walk over an array. For anything richer — transforming, filtering, or searching — array methods are almost always clearer than a hand-written loop:

const nums = [3, 7, 1, 9, 4];

nums.length; // 5
nums.includes(9); // true
nums.indexOf(1); // 2
[...nums].sort((a, b) => a - b); // [1, 3, 4, 7, 9] — sort mutates in place, so copy first!
nums.push(12); // adds to the end, mutates
nums.pop(); // removes from the end, mutates, returns the removed value
nums.slice(1, 3); // [7, 1] — a new array, original untouched
nums.join(', '); // "3, 7, 1, 9, 4"

sort deserves a callout: by default it sorts elements as strings (so [10, 2, 1].sort() gives [1, 10, 2], not [1, 2, 10]), and it mutates the array it's called on. Always pass a comparator for numbers, and copy first ([...nums].sort(...)) if you need to preserve the original order.

We'll cover map, filter, and reduce — the three methods you'll use the most — in full in Chapter 5, since they deserve dedicated attention.

Strings and their properties

Strings share some of the same reading operations as arrays (.length, indexing, .slice), because under the hood a string behaves like a read-only sequence of characters. But a string isn't a "real" array — it has no push, pop, or other mutating methods, precisely because strings are immutable (Chapter 1).

Rest parameters

The ... syntax, when used in a function's parameter list, collects any number of remaining arguments into a real array:

function total(...amounts) {
return amounts.reduce((sum, n) => sum + n, 0);
}

total(1, 2, 3); // 6
total(10, 20, 30, 40); // 100

The same ... syntax used the other way — inside an array or function call — spreads an iterable out into individual elements, which is how you combine or copy arrays concisely:

const morning = [1, 2, 3];
const evening = [4, 5, 6];
const wholeDay = [...morning, ...evening]; // [1, 2, 3, 4, 5, 6]

The Math object

Math is a built-in object bundling numeric constants and functions — not a class you instantiate, just a namespace:

Math.max(4, 9, 2);   // 9
Math.min(4, 9, 2); // 2
Math.round(4.6); // 5
Math.floor(4.6); // 4
Math.random(); // a pseudo-random number, 0 (inclusive) to 1 (exclusive)
Math.PI; // 3.14159...

Destructuring

Destructuring pulls values out of objects or arrays directly into named bindings, which cuts down on repetitive entry.x, entry.y boilerplate:

const { date, hoursSlept } = entry;
console.log(`${date}: ${hoursSlept}h`);

const [first, second, ...rest] = [10, 20, 30, 40];
// first = 10, second = 20, rest = [30, 40]

It also works directly in function parameters, which is extremely common in real codebases:

function summarize({ date, hoursSlept }) {
return `${date}: ${hoursSlept}h of sleep`;
}
summarize(entry);

Optional property access

Accessing a nested property when an intermediate value might not exist used to require a chain of manual checks. The ?. (optional chaining) operator does it in one step, short-circuiting to undefined instead of throwing:

const user = { profile: { name: 'Kai' } };

user.profile?.name; // "Kai"
user.settings?.theme; // undefined — no error, even though "settings" doesn't exist
user.settings?.theme ?? 'light'; // "light" — combine with ?? for a clean default

JSON: reading and writing data as text

JSON (JavaScript Object Notation) is a text format for representing objects and arrays — the de facto standard for sending structured data between a server and a browser, or saving it to a file. It looks almost exactly like JavaScript object/array syntax, with a couple of restrictions (property names must be double-quoted strings, and functions/undefined aren't representable).

const data = { name: 'Kai', hoursSlept: [7, 6.5, 8] };

const text = JSON.stringify(data);
// '{"name":"Kai","hoursSlept":[7,6.5,8]}'

const parsedBack = JSON.parse(text);
// { name: 'Kai', hoursSlept: [7, 6.5, 8] } — a brand-new object, not the same reference

JSON.stringify(data, null, 2) adds indentation, which is handy for logging readable output during debugging.

Key takeaways

  • Objects model "a thing with named properties"; arrays model "an ordered list of things" — most real data is a combination of the two.
  • const prevents reassigning the binding, not mutating the object or array it points to.
  • === on objects/arrays compares identity, not contents — two separately created objects are never ===, even with identical properties.
  • Destructuring and optional chaining (?.) dramatically cut down boilerplate when reading nested data.
  • JSON.stringify/JSON.parse convert between JavaScript values and their text representation.

Try it yourself

  1. Given const users = [{name: 'A', age: 30}, {name: 'B', age: 17}], write an expression that returns just the names of users 18 or older.
  2. Write a function mergeSettings(defaults, overrides) that returns a new object combining both, with overrides winning on any shared key. (Hint: the spread operator works on objects too.)
  3. Destructure { a, b, ...rest } from { a: 1, b: 2, c: 3, d: 4 } and log all three bindings.
Hints
  1. You'll want .filter(...) followed by .map(...) — both covered fully in Chapter 5, but you can write the loop version with for...of and an if for now.
  2. { ...defaults, ...overrides } — later spreads override earlier ones for the same key.
  3. rest will end up as { c: 3, d: 4 } — the rest pattern collects everything not explicitly named.

Summary

Objects and arrays are the workhorses of every JavaScript program — almost everything you model, from a user profile to an entire API response, is built from these two shapes. Understanding mutability and reference semantics now will save you from a whole category of "why did this other variable change?!" bugs later. Next, we look at what you can do with arrays once you stop reaching for manual loops: higher-order functions.

 

🌟 Join the JavaScript360 community

Connect with fellow JS developers, and get free interview prep, React challenges, and DSA guides.

Join on WhatsApp →