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
constdoesn'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 on — logger, 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.
constprevents 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.parseconvert between JavaScript values and their text representation.
Try it yourself
- Given
const users = [{name: 'A', age: 30}, {name: 'B', age: 17}], write an expression that returns just the names of users 18 or older. - Write a function
mergeSettings(defaults, overrides)that returns a new object combining both, withoverrideswinning on any shared key. (Hint: the spread operator works on objects too.) - Destructure
{ a, b, ...rest }from{ a: 1, b: 2, c: 3, d: 4 }and log all three bindings.
Hints
- You'll want
.filter(...)followed by.map(...)— both covered fully in Chapter 5, but you can write the loop version withfor...ofand aniffor now. { ...defaults, ...overrides }— later spreads override earlier ones for the same key.restwill 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.