Objects

A collection of related values, stored as named properties.

What is it?

An array is great when your data is an ordered list, but a lot of real data isn't a list — it's a group of related details, like a person's name, age, and email. An object stores values under named keys instead of numbered positions, so you can describe something with multiple properties in one place.

Explain like I'm 10

If an array is a row of numbered lockers, an object is a labeled filing cabinet — each drawer has a name on it (like "name" or "age"), and you open the one you need by its label, not its position.

Examples

Creating and using an object

const user = {
  name: "Amara",
  age: 28,
  isAdmin: false,
};

console.log(user.name);      // "Amara"
console.log(user["age"]);    // 28

user.age = 29; // update a property

Properties can be read with dot notation (user.name) or bracket notation (user["age"]), which is useful when the key is dynamic.

Objects with methods

const user = {
  name: "Amara",
  greet() {
    console.log("Hi, I'm " + this.name);
  },
};

user.greet(); // "Hi, I'm Amara"

A function stored as a property is called a method. Inside it, this refers back to the object it was called on.

How it works

An object stores each value under a key. When you write user.name, JavaScript looks up the key "name" inside the object and returns whatever value is stored there. Unlike arrays, there's no guaranteed numeric order — you access things by name, not position.

One important detail: a variable holding an object doesn't hold the object itself, it holds a reference — directions to where the object actually lives. So copying that variable with = only copies the directions, not the object: both variables end up pointing at the exact same object, and changing one is visible through the other.

Why does it exist?

Real-world things usually have multiple attributes at once — a product has a name, price, and stock count; a user has an email and a role. Objects let you group all of that related data together instead of tracking it in several separate, disconnected variables.

When to use it

Use an object whenever you're describing one thing with several named attributes — a user, a product, a settings config. If you'd naturally answer "what properties does it have?" rather than "what position is it at?", it's an object.

When not to use it

If your data is really a sequence of similar items, an array (or an array of objects) fits better than a single object with numbered-looking keys. And for very large collections you need to search by key constantly, a Map can be a better fit than a plain object.

Common mistakes

  • Trying to access a property that doesn't exist and being surprised it returns undefined instead of an error.

  • Confusing objects with arrays — using numeric indexes on an object won't work the way you expect.

  • Forgetting that copying an object with = copies a reference, not a brand-new independent object.

Practice exercises

  1. Easy:

    Create an object representing a book with title, author, and pages properties.

  2. Medium:

    Write a function that takes a user object and returns a greeting string using its name property.

  3. Hard:

    Write a function that takes an array of objects (e.g. products) and returns only the ones where inStock is true.

Interview questions

What is an object in JavaScript, and how is it different from an array?

A collection of values stored under named keys (properties) rather than numeric positions — you retrieve a value by its key, not by where it sits in a sequence, which fits data that's a set of attributes rather than an ordered list.

When is bracket notation required instead of dot notation?

When the key is stored in a variable (user[key]), computed at runtime, or isn't a valid identifier — for example user["first-name"] can't be written as user.first-name, since that would parse as subtraction.

How do you check whether a key exists on an object?

"name" in user checks own and inherited keys; Object.hasOwn(user, "name") checks only the object's own properties. Checking user.name !== undefined is unreliable, since a key can exist with the value undefined.

What does this log? `const obj = {}; console.log("toString" in obj); console.log(Object.hasOwn(obj, "toString"));`

true then false — every object inherits toString from Object.prototype, so in (which checks the whole prototype chain) finds it, but Object.hasOwn only looks at properties defined directly on obj itself.

What do `Object.keys()`, `Object.values()`, and `Object.entries()` each return?

Object.keys(obj) returns an array of the object's own enumerable property names; Object.values(obj) returns the corresponding values; Object.entries(obj) returns [key, value] pairs, which is what you loop over with for...of to get both at once.

What does this log? `const a = {}; const b = a; b.x = 1; console.log(a.x);`

1 — like arrays, objects are reference types. b = a copies the reference, so a and b point to the exact same object, and a mutation through b is visible through a.

Why does `{} === {}` evaluate to `false`?

=== compares object references, not structural content — two separate object literals are two separate allocations in memory, so they're never === to each other even with identical properties.

How do you make a shallow copy of an object?

Object spread ({ ...obj }) or Object.assign({}, obj) — both copy each top-level property into a new object, but any nested object or array values are still shared references with the original.

What does this log? `const original = { info: { age: 20 } }; const copy = { ...original }; copy.info.age = 21; console.log(original.info.age);`

21. Spread only copies one level deep — copy.info and original.info are still the same nested object, so mutating it through one is visible through the other.

How would you actually deep-clone an object, and what are the trade-offs?

structuredClone(obj) handles most values (including dates, nested objects/arrays, and cycles) natively; JSON.parse(JSON.stringify(obj)) also deep-clones but silently drops functions and undefined values, converts dates to strings, and throws on circular references.

What determines what `this` refers to inside an object method?

How the method is called, not where it's defined — calling it as obj.method() binds this to obj for that call; the same function called any other way (assigned to a variable, passed as a callback) gets a different this, or none at all.

What does this log? `const user = { name: "Amara", greet() { console.log(this.name); } }; const greet = user.greet; greet();`

undefined (or a TypeError in strict mode/modules). Assigning user.greet to a bare variable detaches it from user — called as a plain function, this is no longer bound to user, so this.name isn't user.name anymore.

What does this log, and why is it a common mistake? `const obj = { name: "Amara", greet: () => console.log(this.name) };` then calling `obj.greet()`

It logs undefined. Arrow functions don't have their own this — they capture this lexically from the surrounding scope where the object literal was written (typically module or global scope), never from the object they're attached to.

What is a computed property name, and when do you need one?

{ [key]: value } syntax lets you use the value of an expression as a property key when building an object literal, instead of only static, hand-typed key names — useful when the key comes from a variable or function argument.

What is object shorthand syntax?

When a property's key and the variable providing its value share the same name, you can write just { name } instead of { name: name }; method shorthand similarly lets you write greet() {} instead of greet: function () {}.

What does this log? `const user = {}; console.log(user.address?.city);`

undefined, without throwing. ?. short-circuits the moment it hits a nullish (null/undefined) link in the chain — since user.address is undefined, the whole expression short-circuits to undefined instead of trying undefined.city and throwing.

What's the difference between `??` and `||` when supplying a default value?

|| falls back whenever the left side is any falsy value (0, "", false, null, undefined); ?? (nullish coalescing) falls back only for null or undefined, so a legitimately falsy value like 0 or "" is preserved.

What does `Object.freeze()` do, and what's a common misconception about it?

It prevents adding, removing, or reassigning an object's own top-level properties. The misconception is that it deep-freezes — it doesn't: nested objects inside a frozen object are completely unaffected and remain fully mutable.

What does this log? `const obj = Object.freeze({ a: { b: 1 } }); obj.a.b = 2; console.log(obj.a.b);`

2. Object.freeze only locks the object it's called on directly — the nested object at obj.a was never frozen, so its own property can still be reassigned.

What's the difference between declaring an object with `const` and freezing it with `Object.freeze()`?

const only prevents the variable from being reassigned to a different value — the object it points to can still be mutated freely. Object.freeze() is what actually stops the object's own properties from changing; the two solve different problems and are often used together.

What's the difference between the `in` operator/`for...in` and `Object.keys()` for checking or iterating properties?

Both in and for...in walk the entire prototype chain, so they can surface inherited properties you didn't intend to include; Object.keys() (and Object.entries()) only returns the object's own enumerable properties, which is almost always what you actually want.

Why can't you call `.map()` or `.filter()` directly on a plain object?

Those are array methods — a plain object has no built-in iteration protocol or index-based structure for them to operate on. To transform an object's data with array methods, convert it first with Object.entries(), run .map()/.filter() on the resulting pairs, then rebuild it with Object.fromEntries().

What does this log? `const a = { x: 1, y: 2 }; const b = { y: 3, z: 4 }; console.log({ ...a, ...b });`

{ x: 1, y: 3, z: 4 } — when spreading multiple sources into one object literal, later properties overwrite earlier ones with the same key, so b's y wins over a's.

What's the difference between `Object.assign(target, source)` and `{ ...source }`?

Object.assign mutates and returns its first argument (target), copying source's properties into it; object spread always produces a brand-new object, leaving every argument untouched — passing {} as the target to Object.assign is what makes it behave non-mutating, like spread.

How do you rename a property while destructuring it out of an object?

const { name: userName } = user; pulls the name property out but binds it to a local variable called userName instead — useful for avoiding naming collisions or matching a more descriptive local name.

What does this log? `function updateAge(person) { person.age = 30; } const p = { age: 20 }; updateAge(p); console.log(p.age);`

30. Objects passed as function arguments are references — person inside the function points to the same object as p, so mutating a property through it is visible to the caller, the same way it is with arrays.

If reassigning a function parameter to a new object doesn't affect the caller, but mutating it does, what's actually being copied when an object is passed to a function?

The reference itself is copied by value — the function gets its own copy of the pointer to the object, not the object. Reassigning that local pointer to something else doesn't touch the original object or the caller's variable; changing a property on the object it still points to does, because both pointers refer to the same object.

Why does using `JSON.stringify()` to compare two objects for equality sometimes give a false negative?

JSON.stringify preserves key insertion order, so two objects with the same properties added in a different order produce different strings even though they're logically equal; it also can't represent undefined values, functions, or symbols consistently.

Why does a plain object literal like `{}` already have methods such as `toString()` that you never defined?

Every plain object literal automatically inherits from Object.prototype unless created otherwise (e.g. Object.create(null)), which is why methods like toString() and hasOwnProperty() work on it even though you never wrote them yourself.

What's a practical use for a `Symbol` as an object property key?

A Symbol key is guaranteed unique and is skipped by Object.keys(), for...in, and JSON.stringify() — useful for attaching metadata or 'hidden' internal state to an object without risking a name collision with its regular, visible properties.

How would you loop over both the keys and values of an object at once?

for (const [key, value] of Object.entries(obj)) { ... } — Object.entries() turns the object into an array of [key, value] pairs, which for...of can destructure directly on each pass.

What happens if you try to use an object as a property key on another plain object?

The key gets coerced to a string first — since a plain object's keys are always strings (or symbols) — so any object used as a key becomes the literal string "[object Object]", meaning different objects used as keys can silently collide into the same property. A Map is the right tool when you need actual objects as keys.

What happens if you destructure a property that doesn't exist on the object, and no default is given?

The resulting variable is simply undefined — destructuring a missing key behaves exactly like accessing a missing property directly (obj.missing), it doesn't throw.

Can an object property's default value in destructuring reference another property being destructured in the same pattern?

No — each default expression is evaluated independently and can only see variables already in scope outside the pattern (or earlier parameters, in a function signature); it can't reference a sibling property from the same destructuring pattern.