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 tokey3: 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
objitself has an own property calledsomeProperty(this is whatObject.hasOwn(obj, "someProperty")or the olderobj.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 ofObject.prototype), at which point the result isundefined. - Writing to
obj.someProperty = valueis different: it does not walk the prototype chain to find where to write. It creates or updates an own property directly onobj, 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 ofnew 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 repeatingname: nameor writinggreet: function() {}. - Use the spread operator (
{ ...obj }) orObject.assign({}, obj)for shallow copies, and reach forstructuredClone(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(), andObject.entries()to iterate objects rather thanfor...in, which also walks inherited enumerable properties and can surprise you on objects with a custom prototype. - Prefer
Object.hasOwn(obj, key)overobj.hasOwnProperty(key)— it works even whenobjhas no prototype (e.g., created withObject.create(null)).
Practice Exercises
- Exercise 1: Create an object
bookwith propertiestitle,author, andpagesRead. Write a methodpercentComplete(totalPages)on the object that returnspagesReaddivided bytotalPagesas a percentage. - Exercise 2: Given
const settings = { theme: { mode: "dark" } };, write code that creates a copy ofsettingswhere changing the copy’stheme.modedoes 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 afor...inloop.
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., withstructuredClone()). - 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-inObject.keys/values/entriesmethods to write safer, more idiomatic object code.
