Skip to content

JavaScript lesson 7 of 9

JavaScript Objects

Learn how JavaScript objects pair keys with values, read them safely with optional chaining, add, update, and delete properties, loop with Object.entries, and why lookups are fast.

Published · Every example on this page was run before it was published.

An array is great when the thing you care about is position — the first score, the last item, everything from index 2 onward. But a lot of real data has no meaningful position at all. If you store a person's phone number, you don't want to remember that it lives at index 47; you want to look it up by their name. When the natural way to find a value is by a label rather than by a slot number, you want an object.

What Is an Object?

An object is a collection that stores pairs: each key (the label you look something up by) points to a value (the data stored under that label). Together one key and its value are called a property. Instead of asking "what's at position 3?", you ask "what's stored under Grace?"

JavaScript writes an object with curly braces {}, each property written as key: value, and the properties separated by commas:

JavaScript
const phoneBook = { Ada: "555-0101", Grace: "555-0142", Linus: "555-0199" };
console.log(phoneBook.Grace);
console.log(phoneBook["Grace"]);
Output
555-0142
555-0142

Both lines read the same property two different ways. Dot notation, phoneBook.Grace, is shorter and is what you'll use most of the time. Bracket notation, phoneBook["Grace"], does the same lookup, but it can also take a variable — dot notation cannot, because whatever follows the dot is always treated as a literal property name:

JavaScript
const phoneBook = { Ada: "555-0101", Grace: "555-0142", Linus: "555-0199" };
const key = "Grace";
console.log(phoneBook[key]);
Output
555-0142

phoneBook.key would have looked for a property literally called "key", which does not exist. Reach for bracket notation whenever the property name is stored in a variable rather than typed directly.

Missing Properties Don't Throw

Here is a genuinely important difference from arrays and strings, which you have already seen return undefined for an out-of-range index: JavaScript objects behave the same way for a missing key. There is no error, ever, for asking about a property that isn't there — you simply get undefined back:

JavaScript
const phoneBook = { Ada: "555-0101", Grace: "555-0142", Linus: "555-0199" };
console.log(phoneBook.Zoe);
console.log(phoneBook["Zoe"]);
Output
undefined
undefined

This means you never need a special "safe lookup" method the way some other data structures require — plain property access is already safe. If you want a fallback value instead of a bare undefined, the nullish coalescing operator, ??, supplies one: it returns its left side unless that side is null or undefined, in which case it returns the right side.

JavaScript
const phoneBook = { Ada: "555-0101", Grace: "555-0142", Linus: "555-0199" };
console.log(phoneBook.Zoe ?? "not in the book");
Output
not in the book

Adding, Updating, and Deleting Properties

Objects are mutable. Adding a new property and updating an existing one use exactly the same syntax — assignment into a key. JavaScript decides which one happens based on whether that key already exists:

JavaScript
const scores = { Ana: 10 };
scores.Ben = 8;
scores.Ana = 12;
console.log(scores);
Output
{ Ana: 12, Ben: 8 }

scores.Ben = 8 created a brand-new property because Ben wasn't there yet. scores.Ana = 12 overwrote the existing value, replacing 10 with 12 without disturbing its position.

To remove a property entirely, use the delete keyword:

JavaScript
const inventory = { bolts: 40, nuts: 25, washers: 60 };
delete inventory.washers;
console.log(inventory);
Output
{ bolts: 40, nuts: 25 }

Before reaching for a key, you can ask whether it's there at all with the in operator, which checks property names, never values:

JavaScript
const inventory = { bolts: 40, nuts: 25 };
console.log("bolts" in inventory);
console.log("washers" in inventory);
Output
true
false

Looping Through an Object

An object does not hand you its contents with a plain for...of loop the way an array does — you need to say which part you want first. Object.keys() gives the property names, Object.values() gives the stored data, and Object.entries() gives both together as an array of [key, value] pairs:

JavaScript
const prices = { tea: 3, coffee: 5, juice: 4 };
console.log(Object.keys(prices));
console.log(Object.values(prices));
console.log(Object.entries(prices));
Output
[ 'tea', 'coffee', 'juice' ]
[ 3, 5, 4 ]
[ [ 'tea', 3 ], [ 'coffee', 5 ], [ 'juice', 4 ] ]

All three return real arrays, so every array method and every loop form you already know works on the result. Object.entries() is the one you'll use most, because it pairs naturally with destructuring two loop variables straight out of each pair:

JavaScript
const prices = { tea: 3, coffee: 5, juice: 4 };
let total = 0;
for (const [drink, price] of Object.entries(prices)) {
  console.log(`${drink} costs ${price}`);
  total += price;
}
console.log("Total:", total);
Output
tea costs 3
coffee costs 5
juice costs 4
Total: 12

Because Object.values() produces a plain array of the data, every array-friendly tool works on it directly:

JavaScript
const prices = { tea: 3, coffee: 5, juice: 4 };
console.log(Math.max(...Object.values(prices)));
Output
5

What Can Be a Key?

Object keys are always strings under the hood (or a less common type called Symbol), even when you write something that looks like a number. JavaScript silently converts a numeric key to its string form:

JavaScript
const locations = {};
locations.home = "warm";
locations.office = "cold";
locations[7] = "lucky number";
console.log(locations);
console.log(Object.keys(locations));
Output
{ '7': 'lucky number', home: 'warm', office: 'cold' }
[ '7', 'home', 'office' ]

Notice that '7' printed first, even though it was the last property added. This is a genuine JavaScript quirk worth knowing about: a key that looks like a non-negative whole number is always listed before any word-based key, in ascending numeric order, regardless of when it was actually added. Word-based keys like home and office keep their normal insertion order among themselves. This is one good reason to reach for an array, not an object, whenever your data is naturally numbered — and to reserve object keys for named fields.

JavaScript
const locations = { home: "warm", office: "cold", 7: "lucky number" };
console.log(typeof Object.keys(locations)[0]);
Output
string

Even the key 7, written as a bare number in the source code, comes back out of Object.keys() as the string "7".

Nesting Objects

An object's value can be another object, or an array, or both — that's how you model something with real structure, like a record per person where each record has its own named fields:

JavaScript
const students = {
  ana: { age: 21, grades: [88, 92] },
  ben: { age: 22, grades: [75, 80] },
};
console.log(students.ana.age);
console.log(students.ben.grades[1]);
students.ana.grades.push(95);
console.log(students.ana);
Output
21
80
{ age: 21, grades: [ 88, 92, 95 ] }

Read the chained dots strictly left to right. students.ana produces the inner object { age: 21, grades: [88, 92] }, and the next .age looks up a property in that result. The danger with nesting is that a missing piece anywhere in the chain can crash the rest of it — reading .age off a student that doesn't exist tries to read a property off undefined, which throws. The optional chaining operator, ?., short-circuits the whole chain to undefined the instant it hits a missing link, instead of crashing:

JavaScript
const students = {
  ana: { age: 21, grades: [88, 92] },
  ben: { age: 22, grades: [75, 80] },
};
console.log(students.cleo?.age);
console.log(students.cleo?.age ?? "unknown");
Output
undefined
unknown

students.cleo does not exist, so students.cleo?.age stops right there and evaluates to undefined rather than trying to read .age off nothing. Pairing ?. with ?? — "if any link is missing, fall back to this" — is one of the most useful small combinations in modern JavaScript.

Why Object Lookups Are Fast

Here's the property that makes objects central to problem solving, not just to storing records.

To check whether a value is in an array, JavaScript has no choice but to walk the array and compare values one by one. If the array holds ten thousand numbers and the one you want sits at the end, that's ten thousand comparisons. An object doesn't search at all — it computes where a key belongs and jumps directly there, which is why property lookup is described as taking roughly constant time, regardless of how many properties the object holds.

That difference unlocks a pattern you'll use constantly: instead of re-scanning data you've already looked at, remember it in an object as you go, then ask one instant question per new item. Here's the pattern finding duplicates in a single pass:

JavaScript
const words = ["red", "blue", "red", "green", "blue", "red"];
const seen = {};
const duplicates = [];

for (const word of words) {
  if (word in seen) {
    duplicates.push(word);
  } else {
    seen[word] = true;
  }
}

console.log(duplicates);
console.log(seen);
Output
[ 'red', 'blue', 'red' ]
{ red: true, blue: true, green: true }

The loop touches each word exactly once. For every word it asks the seen object a single instant question — have I met you before? — instead of rescanning the earlier part of the array. This is precisely the idea behind the Two Number Sum practice problem linked at the end of this lesson. That problem hands you an array of integers and a target, and asks for the pair that adds up to the target. The slow approach compares every number against every other number. The fast approach scans the array once and, for each number, uses simple arithmetic to work out which partner value would complete the target — then asks an object of already-seen numbers whether that partner has gone by already. Same shape as the loop above: one pass, one instant lookup per item.

A Worked Example

Here is a small program that tallies how many times each item appears in a shopping cart and reports the most-bought one. It combines ?? for a safe default, Object.entries() iteration, and the in operator.

JavaScript
function tally(items) {
  const counts = {};
  for (const item of items) {
    const key = item.toLowerCase();
    counts[key] = (counts[key] ?? 0) + 1;
  }
  return counts;
}

function mostCommon(counts) {
  let bestItem = "";
  let bestCount = 0;
  for (const [item, count] of Object.entries(counts)) {
    if (count > bestCount) {
      bestItem = item;
      bestCount = count;
    }
  }
  return [bestItem, bestCount];
}

const cart = ["apple", "Banana", "apple", "kiwi", "banana", "apple"];
const counts = tally(cart);
console.log(counts);

const [topItem, topCount] = mostCommon(counts);
console.log(`Most bought: ${topItem} (${topCount})`);
console.log("Is 'mango' in the cart?", "mango" in counts);
console.log("Times 'kiwi' appears:", counts.kiwi ?? 0);
Output
{ apple: 3, banana: 2, kiwi: 1 }
Most bought: apple (3)
Is 'mango' in the cart? false
Times 'kiwi' appears: 1

Walking through it: tally() lowercases every item first, so "Banana" and "banana" land on the same key rather than becoming two separate entries. The single most important line is counts[key] = (counts[key] ?? 0) + 1. Read the right side first: counts[key] ?? 0 reads the current count and falls back to 0 only when the key genuinely does not exist yet — using || instead of ?? here would have been a subtle bug, because a count of 0 is a value || would also treat as "missing" and override, while ?? correctly leaves a real 0 alone.

mostCommon() walks the finished tally with Object.entries(), unpacking each pair into item and count directly in the loop header. It keeps a running champion, starting bestCount at 0 so the first real count always beats it. Because "apple" was counted three times and nothing beat it, that's what comes back. The last two lines show the two read styles side by side: "mango" in counts is a membership test that returns a boolean and never errors, and counts.kiwi ?? 0 is a value lookup with a safe default, which would have returned 0 just as calmly if "kiwi" had never appeared.

Common Mistakes

Chaining a property or method off a value that turned out to be missing

JavaScript
const config = { host: "localhost" };
console.log(config.port);
console.log(config.port.toFixed(0));
Output
undefined
TypeError: Cannot read properties of undefined (reading 'toFixed')

config.port alone is safe and simply evaluates to undefined, exactly as this lesson has shown throughout. The crash comes one step later, when the code tries to call .toFixed() on that undefined value. This is exactly the situation ?. exists to prevent:

JavaScript
const config = { host: "localhost" };
console.log(config.port?.toFixed(0));
Output
undefined

Using || for a default when 0 is a legitimate value

JavaScript
const settings = { volume: 0 };
console.log(settings.volume || 10);
console.log(settings.volume ?? 10);
Output
10
0

settings.volume is 0, a real, meaningful setting — muted. || falls back to its right side whenever the left side is falsy, and 0 is falsy, so settings.volume || 10 throws away the real value and reports 10 instead. ?? only falls back when the left side is null or undefined, so it correctly reports the actual volume. Reach for ?? whenever 0, "", or false could be a genuine value rather than a sign that something is missing.

Comparing two objects with === and expecting matching content to count

JavaScript
const point1 = { x: 1, y: 2 };
const point2 = { x: 1, y: 2 };
console.log(point1 === point2);
console.log(point1 === point1);
Output
false
true

point1 and point2 hold identical data but are two separate objects in memory, and === between objects checks whether both sides are literally the same object, not whether their contents match. Comparing an object against itself is true for the obvious reason that there is only one object involved. If you need to compare contents rather than identity, you have to check the properties you care about individually, or convert both sides with JSON.stringify() and compare the resulting strings — a common shortcut that works well for simple data.

Next Steps

Objects turn "search through everything" into "ask one question," and that shift is what separates a slow solution from a fast one in most coding problems. The Two Number Sum practice problem is the natural next step: try the straightforward approach of comparing every pair first, then rewrite it using an object of numbers you've already seen and notice how much work disappears.

Before that, open the JavaScript playground and experiment with the examples here on your own data. Build a small object, deliberately look up a key that doesn't exist, then chain a method off that missing lookup to see the TypeError for real, and rewrite it with ?. to make it safe. Getting a feel for when each tool is the right one will make the practice problem much smoother.

When you want a harder one

Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.