JavaScript Objects: A Comprehensive Guide
Objects are the fundamental data structure in JavaScript. Everything that is not a primitive (string, number, boolean, null, undefined, symbol, bigint) is an object. Understanding objects deeply is essential for mastering JavaScript.
Creating Objects
// Object literal (most common)
const user = {
name: 'Alice',
age: 30,
greet() {
return `Hello, I'm ${this.name}`;
},
---;
// Computed property keys
const key = 'email';
const person = {
name: 'Bob',
[key]: 'bob@example.com', // computed from variable
[`${key}_verified`]: true,
---;
// Object.create
const proto = { greet() { return 'Hi'; } };
const obj = Object.create(proto);
obj.name = 'Charlie';
// Object.fromEntries
const entries = [['name', 'Diana'], ['age', 25]];
const fromEntries = Object.fromEntries(entries);
// Constructor function
function User(name, age) {
this.name = name;
this.age = age;
---
const user = new User('Alice', 30);
// Class syntax (syntactic sugar)
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
---Object literals are preferred for simple data containers. Classes and constructor functions are better when you need methods shared through prototypes.
Property Descriptors
Every property has a descriptor that defines its behavior:
const obj = { name: 'Alice' };
// Get descriptor
console.log(Object.getOwnPropertyDescriptor(obj, 'name'));
// { value: 'Alice', writable: true, enumerable: true, configurable: true }
// Define property with custom descriptor
Object.defineProperty(obj, 'id', {
value: 123,
writable: false, // cannot be reassigned
enumerable: false, // hidden from for...in and Object.keys
configurable: false, // cannot be deleted or reconfigured
---);
// Define multiple properties
Object.defineProperties(obj, {
created: {
value: new Date(),
writable: false,
},
updated: {
value: null,
writable: true,
},
---);Property Descriptor Flags
writable— can the value be changed?enumerable— does it show up infor...inandObject.keys?configurable— can the property be deleted or reconfigured?
Once configurable is set to false, it cannot be changed back. This is used by JavaScript engines for built-in objects and is useful for library APIs that need guaranteed behavior.
Getters and Setters
const user = {
firstName: 'Alice',
lastName: 'Smith',
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(' ');
},
---;
console.log(user.fullName); // 'Alice Smith' (no function call)
user.fullName = 'Bob Jones';
console.log(user.firstName); // 'Bob'
Getters and setters make computed properties behave like regular properties. They are evaluated lazily — the getter runs only when the property is accessed.
Prototypes and Inheritance
const animal = {
eat() {
console.log('Eating...');
},
---;
const dog = Object.create(animal);
dog.bark = function() {
console.log('Woof!');
---;
dog.eat(); // 'Eating...' — inherited from prototype
dog.bark(); // 'Woof!' — own property
// Prototype chain: dog → animal → Object.prototype → null
console.log(Object.getPrototypeOf(dog) === animal); // true
console.log(animal.isPrototypeOf(dog)); // true
Modern Inheritance with Classes
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
---
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
console.log(`${this.name} barks!`);
}
---
const dog = new Dog('Rex', 'German Shepherd');
dog.speak(); // 'Rex barks!'
Object Destructuring
const user = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
address: {
city: 'New York',
zip: '10001',
},
---;
// Basic destructuring
const { name, email } = user;
// Renaming
const { name: userName, email: userEmail } = user;
// Default values
const { role = 'user' } = user;
// Nested destructuring
const { address: { city, zip } } = user;
// Rest pattern
const { id, ...rest } = user;
console.log(rest); // { name: 'Alice', email: '...', address: {...} }
// Function parameter destructuring
function printUser({ name, email, role = 'user' }) {
console.log(`${name} (${email}) - ${role}`);
---Destructuring is particularly useful for function parameters — callers pass an options object and the function extracts only what it needs.
Spread and Rest with Objects
// Spread — shallow copy
const original = { a: 1, b: 2 };
const copy = { ...original };
console.log(copy === original); // false — different reference
// Merging objects (later keys override earlier ones)
const merged = { ...defaults, ...userSettings, ...runtimeOverrides };
// Cloning with modifications
const updated = { ...user, name: 'Bob' };
// Rest in destructuring
const { password, ...safeUser } = user;
// safeUser contains everything except password
Spread creates a shallow copy — nested objects are shared between the original and the copy. For deep cloning, use structuredClone(obj) which handles all built-in types including Date, Map, Set, and ArrayBuffer.
Object Methods
const obj = { a: 1, b: 2, c: 3 };
// Keys, values, entries
console.log(Object.keys(obj)); // ['a', 'b', 'c']
console.log(Object.values(obj)); // [1, 2, 3]
console.log(Object.entries(obj)); // [['a',1], ['b',2], ['c',3]]
// fromEntries — reverse of entries
const from = Object.fromEntries([['a', 1], ['b', 2]]);
// Iteration
// for...in (includes inherited enumerable properties)
for (const key in obj) {
if (Object.hasOwn(obj, key)) { // safer than hasOwnProperty
console.log(key, obj[key]);
}
---
// Object.keys + forEach (own properties only)
Object.keys(obj).forEach(key => {
console.log(key, obj[key]);
---);Object.hasOwn(obj, key) is the modern replacement for obj.hasOwnProperty(key) — it works correctly even if the object overrides hasOwnProperty or was created with Object.create(null).
Object Equality
const a = { x: 1, y: { z: 2 } };
const b = { x: 1, y: { z: 2 } };
// Reference equality
console.log(a === b); // false
// Shallow equality
function shallowEqual(obj1, obj2) {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
return keys1.every(key => obj1[key] === obj2[key]);
---
// Deep equality (for trusted/simple structures)
console.log(JSON.stringify(a) === JSON.stringify(b)); // true — but fragile
// Structured comparison (Node 22+/Chrome 120+)
// import { isDeepStrictEqual } from 'util';
Frozen, Sealed, and Extensible
const obj = { a: 1 };
// Object.preventExtensions — cannot add new properties
Object.preventExtensions(obj);
obj.b = 2; // silently fails (strict mode: TypeError)
// Object.seal — preventExtensions + non-configurable properties
Object.seal(obj);
delete obj.a; // silently fails
// Object.freeze — seal + non-writable properties (read-only)
Object.freeze(obj);
obj.a = 99; // silently fails
// Check state
console.log(Object.isExtensible(obj)); // false
console.log(Object.isSealed(obj)); // true
console.log(Object.isFrozen(obj)); // true
Object.freeze is shallow — nested objects are still mutable. Use recursive freezing or libraries like immer for immutable data management.
Property Ordering
ES2015 defined property ordering rules:
const obj = {
z: 1,
a: 2,
[Symbol('id')]: 'secret',
b: 3,
---;
// Integer-like keys (0, 1, 2...) come first, in ascending order
// String keys come in insertion order
// Symbol keys come in insertion order (last)
const mixed = { 2: 'two', 0: 'zero', 1: 'one', a: 'A' };
console.log(Object.keys(mixed)); // ['0', '1', '2', 'a']
// Object.assign maintains source order
const merged = Object.assign({}, { a: 1 }, { b: 2 }, { a: 3 });
console.log(merged); // { a: 3, b: 2 } — last value wins
Proxy for Metaprogramming
const validator = {
set(target, key, value) {
if (key === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('Age must be an integer');
}
if (value < 0 || value > 150) {
throw new RangeError('Age must be 0-150');
}
}
target[key] = value;
return true;
},
get(target, key) {
if (key.startsWith('_')) {
throw new Error('Cannot access private properties');
}
return target[key];
},
deleteProperty(target, key) {
if (key === 'id') {
throw new Error('Cannot delete id property');
}
delete target[key];
return true;
}
---;
const user = new Proxy({ id: 1, name: 'Alice', age: 30 }, validator);
user.age = 31; // OK
// user.age = 'old'; // TypeError
// delete user.id; // Error: Cannot delete id property
FAQ
Q: What is the difference between null and undefined for object properties?
A: null is an explicitly assigned empty value. undefined means the property does not exist. Use null for intentional absence and check existence with 'key' in obj or obj?.key !== undefined.
Q: How do I check if an object has a property?
A: Use Object.hasOwn(obj, 'key') for own properties or 'key' in obj to check the prototype chain. Avoid obj.key !== undefined — it fails for properties explicitly set to undefined.
Q: What is the best way to clone an object?
A: For shallow clones, use {...obj} or Object.assign({}, obj). For deep clones, use structuredClone(obj) (modern) or JSON.parse(JSON.stringify(obj)) (limited — loses functions, undefined, Dates as strings).
Q: How do I make an object immutable?
A: Use Object.freeze() for shallow immutability. For deep immutability, use a library like Immer or freeze recursively.
Q: What is the difference between for…in and Object.keys?
A: for...in iterates over all enumerable properties including inherited ones. Object.keys returns only own enumerable properties.
Q: How do I merge two objects?
A: Use spread: {...obj1, ...obj2} (later keys override). For deeper merges, use a library like lodash.merge or implement recursive assignment.
For a comprehensive overview, read our article on Javascript Arrays Guide.
For a comprehensive overview, read our article on Javascript Async Await.
Related Concepts and Further Reading
Understanding javascript objects requires familiarity with several interconnected ideas and principles that together form a complete picture. Exploring these related concepts deepens your knowledge and provides context that makes the core material more meaningful and applicable. Each concept builds on the others, creating a web of understanding that supports deeper learning and practical application. Taking time to explore how these elements connect reveals patterns that accelerate comprehension and retention of new information.
The relationship between javascript objects and adjacent fields is worth particular attention. Many of the most important insights emerge at the boundaries between disciplines, where ideas from different areas combine to create new approaches and solutions that neither field could produce alone. Exploring these connections pays dividends in both breadth and depth of understanding, revealing patterns and principles that might otherwise remain hidden from view. Cross-disciplinary knowledge is increasingly valued as problems become more complex and interconnected.
For those looking to go beyond introductory material, several excellent resources provide deeper treatment of specific aspects of javascript objects. Academic journals, industry publications, authoritative reference works, and online courses each offer different perspectives and levels of detail. The key is to match your reading to your current learning goals and build knowledge progressively, focusing on quality over quantity in your study materials. A well-chosen resource that matches your current level is worth more than dozens of resources that are too basic or too advanced.