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.
Related Articles
JavaScript
JavaScript Arrays: A Complete Guide
Comprehensive guide to JavaScript arrays covering creation, iteration, transformation, advanced methods like map/filter/reduce, destructuring, and performance best practices.
JavaScript
JavaScript ES6+ Features Every Developer Should Know
A comprehensive guide to modern JavaScript ES6+ features — arrow functions, destructuring, spread operator, modules, classes, and more.
JavaScript
JavaScript Promises: A Complete Guide
Comprehensive guide to JavaScript Promises covering creation, chaining, error handling, concurrency methods (all, race, allSettled, any), and real-world patterns.
JavaScript
JavaScript Async/Await: Understanding Asynchronous Programming
Learn JavaScript async/await from basics to advanced. Covers promises, error handling, parallel execution, and real-world patterns.
JavaScript
JavaScript Closures: Understanding Scope and Closures
Deep dive into JavaScript closures covering lexical scope, practical closure patterns, module pattern, loops with closures, and common pitfalls.