JavaScript Objects

An object in JavaScript is a collection of related data and behavior, stored as key: value pairs. Almost everything you build in JavaScript — a user profile, a shopping cart, the configuration for a chart library, even functions and arrays themselves — is either an object or built out of them. Understanding how objects store data, how they are copied (or rather, not copied) by reference, and how property lookup actually works will save you from some of the most common bugs in JavaScript.

Overview: What Is an Object?

A JavaScript object is an unordered collection of properties. Each property has a key (a string or a Symbol) and a value, which can be any type — a number, a string, an array, a function, or even another object. Objects let you group related data together instead of tracking a pile of separate variables.

Internally, an object is a reference type. When you create an object with {}, the JavaScript engine allocates memory for that object somewhere on the heap, and the variable you assigned it to holds a reference (essentially a pointer) to that location, not the data itself. This is the single most important thing to understand about objects, because it explains why copying and comparing them behaves so differently from copying and comparing primitives like numbers or strings.

Objects also participate in JavaScript’s prototype chain. Every plain object created with {} automatically links to Object.prototype, which is where methods like toString() and hasOwnProperty() actually live. When you access a property, the engine first checks the object itself; if it isn’t found there, it walks up the prototype chain until it finds the property or reaches the end (null).

Syntax

The most common way to create an object is the object literal:

const objectName = {
  key1: "value1",
  key2: 42,
  key3() {
    return "a method";
  }
};
  • objectName — the variable holding a reference to the object.
  • key1, key2, key3 — property names (also called keys). They can be written unquoted if they are valid identifiers, or as strings/computed expressions in brackets for dynamic names.
  • "value1", 42 — the property values, which can be any JavaScript value or expression.
  • key3() { ... } — shorthand method syntax; equivalent to key3: function() { ... }.

Properties are read and written with either dot notation (objectName.key1) when the key is a valid identifier known ahead of time, or bracket notation (objectName["key1"] or objectName[someVariable]) when the key is dynamic or not a valid identifier (like "first-name").

Examples

Example 1: Creating an object with properties and a method.

const person = {
  firstName: "Ada",
  lastName: "Lovelace",
  birthYear: 1815,
  greet() {
    return `Hi, I'm ${this.firstName} ${this.lastName}`;
  }
};

console.log(person.firstName);
console.log(person["lastName"]);
console.log(person.greet());
console.log(Object.keys(person));

Output:

Ada
Lovelace
Hi, I'm Ada Lovelace
[ 'firstName', 'lastName', 'birthYear', 'greet' ]

Here, this inside greet() refers to the object the method was called on (person), so it can reach the object’s own properties. Object.keys() returns an array of every own, enumerable property name, in insertion order for string keys.

Example 2: Nested objects, computed keys, destructuring, and optional chaining.

const dynamicKey = "score";

const player = {
  name: "Kai",
  stats: {
    level: 12,
    [dynamicKey]: 340
  },
  inventory: ["sword", "shield"]
};

const { name, stats: { level, score } } = player;
console.log(name, level, score);

console.log(player.stats?.mana);
console.log(player.equipment?.weapon ?? "no weapon equipped");

player.inventory.push("potion");
console.log(player.inventory);

Output:

Kai 12 340
undefined
no weapon equipped
[ 'sword', 'shield', 'potion' ]

The computed property [dynamicKey] lets you build a key from a variable at creation time. Destructuring pulls nested values straight out of the object into local variables. Optional chaining (?.) safely returns undefined instead of throwing when a nested property path doesn’t exist, and the nullish coalescing operator (??) supplies a fallback only when the left side is null or undefined.

Example 3: References, shallow copies, and freezing.

const original = {
  title: "Learn JS",
  tags: ["objects", "reference"],
  meta: { views: 100 }
};

const reference = original;
reference.title = "Learn JS Deeply";
console.log(original.title);

const shallowCopy = { ...original };
shallowCopy.tags.push("mutated");
console.log(original.tags);

const frozen = Object.freeze({ level: "admin" });
frozen.level = "user";
console.log(frozen.level);

for (const [key, value] of Object.entries({ a: 1, b: 2 })) {
  console.log(key, value);
}

Output:

Learn JS Deeply
[ 'objects', 'reference', 'mutated' ]
admin
a 1
b 2

Assigning original to reference copies the reference, not the object, so mutating one mutates the other — they point to the same memory. The spread operator ({ ...original }) creates a new top-level object, but it’s a shallow copy: nested objects and arrays (like tags) are still shared references, which is why pushing into shallowCopy.tags also changed original.tags. Object.freeze() prevents changes to an object’s own top-level properties; the reassignment to frozen.level silently does nothing (in non-strict mode) because the property became non-writable. Object.entries() returns an array of [key, value] pairs, which pairs nicely with a for...of loop and array destructuring.

How Property Access Works Under the Hood

When you write obj.someProperty, the engine performs these steps:

  • It checks whether obj itself has an own property called someProperty (this is what Object.hasOwn(obj, "someProperty") or the older obj.hasOwnProperty("someProperty") tests for).
  • If not found, it follows the internal [[Prototype]] link to the object’s prototype and repeats the search there.
  • This continues up the prototype chain until the property is found or the chain ends at null (the prototype of Object.prototype), at which point the result is undefined.
  • Writing to obj.someProperty = value is different: it does not walk the prototype chain to find where to write. It creates or updates an own property directly on obj, even if a property with the same name exists further up the chain.

This is also why two separately-created objects with identical contents are never === to each other: equality for objects compares references (memory addresses), not structural content. Only when two variables hold a reference to the very same object in memory will === return true.

Common Mistakes

Mistake 1: Assuming const makes an object immutable. const only prevents the variable from being reassigned to point at a different object — it does nothing to protect the object’s contents.

const config = { debug: false };
config.debug = true;
console.log(config);

Output:

{ debug: true }

The mutation succeeds even though config was declared with const, because we changed a property, not the binding. To actually lock an object down, use Object.freeze() (shallow) or write your own deep-freeze logic for nested structures.

Mistake 2: Comparing objects with === expecting a structural comparison.

const a = { x: 1 };
const b = { x: 1 };
console.log(a === b);
console.log(JSON.stringify(a) === JSON.stringify(b));

Output:

false
true

a and b are different objects living at different memory addresses, so === reports false even though their contents match. Comparing serialized JSON (or a proper deep-equality function, such as lodash.isEqual) checks structure instead of identity — but be aware JSON.stringify-based comparisons break down for values it can’t represent, like functions, undefined, or Symbol keys, and are also sensitive to property order in some engines.

Best Practices

  • Use object literals ({}) instead of new Object() — they’re shorter and unambiguous.
  • Prefer dot notation for known, static keys; reserve bracket notation for dynamic or computed keys.
  • Use shorthand property and method syntax ({ name, greet() {} }) instead of repeating name: name or writing greet: function() {}.
  • Use the spread operator ({ ...obj }) or Object.assign({}, obj) for shallow copies, and reach for structuredClone(obj) when you need a true deep copy without shared nested references.
  • Use optional chaining (?.) and nullish coalescing (??) when reading properties that might not exist, instead of long && guard chains.
  • Use Object.freeze() for configuration objects or constants that should never be mutated after creation.
  • Use Object.keys(), Object.values(), and Object.entries() to iterate objects rather than for...in, which also walks inherited enumerable properties and can surprise you on objects with a custom prototype.
  • Prefer Object.hasOwn(obj, key) over obj.hasOwnProperty(key) — it works even when obj has no prototype (e.g., created with Object.create(null)).

Practice Exercises

  • Exercise 1: Create an object book with properties title, author, and pagesRead. Write a method percentComplete(totalPages) on the object that returns pagesRead divided by totalPages as a percentage.
  • Exercise 2: Given const settings = { theme: { mode: "dark" } };, write code that creates a copy of settings where changing the copy’s theme.mode does not affect the original object. (Hint: a shallow spread won’t be enough here.)
  • Exercise 3: Write a function countOwnProperties(obj) that returns how many own (non-inherited) properties an object has, without using a for...in loop.

Summary

  • Objects store data as key-value pairs and are the backbone of almost all JavaScript programs.
  • Objects are reference types: variables hold a pointer to the object’s location in memory, not the object itself.
  • Copying a variable copies the reference, so mutating through one variable affects every variable pointing to that same object.
  • The spread operator and Object.assign() make shallow copies only; nested objects/arrays are still shared unless you deep-clone (e.g., with structuredClone()).
  • Property reads walk the prototype chain; property writes create or update an own property directly on the object.
  • === compares object identity, not structural equality.
  • Use Object.freeze(), optional chaining, and the built-in Object.keys/values/entries methods to write safer, more idiomatic object code.