Skip to content
Home
JavaScript Objects: A Comprehensive Guide

JavaScript Objects: A Comprehensive Guide

7 min read

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 in for...in and Object.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&mldr;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.

#javascript#objects#prototype#programming#es6