Objects and Property Management

1. Object Literal Syntax and Computed Properties

Feature Syntax ES Version Description
Basic Object Literal { key: value } ES3 Traditional property definition with static keys
Property Shorthand ES6 { name } ES6 Equivalent to { name: name } when variable name matches key
Method Shorthand ES6 { method() {} } ES6 Concise method definition, equivalent to { method: function() {} }
Computed Property ES6 { [expression]: value } ES6 Property name computed at runtime from expression
Computed Method ES6 { [expr]() {} } ES6 Method name computed dynamically
Spread Properties ES2018 { ...obj } ES2018 Copy enumerable properties from source object
Getters/Setters { get prop() {}, set prop(v) {} } ES5 Define accessor properties with custom logic

Example: Modern object literal syntax

// ES6+ object literal features
const name = 'Alice';
const age = 30;
const propKey = 'dynamicKey';

const user = {
    // Property shorthand (ES6)
    name,  // Same as name: name
    age,   // Same as age: age
    
    // Method shorthand (ES6)
    greet() {
        return `Hello, I'm ${this.name}`;
    },
    
    // Computed property name (ES6)
    [propKey]: 'dynamic value',
    [`computed_${name}`]: true,
    
    // Computed method name (ES6)
    [`get${name}Info`]() {
        return `${this.name} is ${this.age}`;
    },
    
    // Traditional syntax still works
    email: 'alice@example.com',
    
    // Arrow function as property (not a method)
    arrowFunc: () => 'arrow',
    
    // Getter and setter
    get fullInfo() {
        return `${this.name}, ${this.age} years old`;
    },
    set fullInfo(info) {
        [this.name, this.age] = info.split(', ');
    }
};

console.log(user.name);  // 'Alice'
console.log(user.greet());  // 'Hello, I'm Alice'
console.log(user.dynamicKey);  // 'dynamic value'
console.log(user.computed_Alice);  // true
console.log(user.getAliceInfo());  // 'Alice is 30'
console.log(user.fullInfo);  // 'Alice, 30 years old'

Example: Computed properties in practice

// Dynamic property names
const fields = ['id', 'name', 'email'];
const values = [1, 'Bob', 'bob@example.com'];

// Build object with computed properties
const user = fields.reduce((obj, field, index) => {
    obj[field] = values[index];
    return obj;
}, {});
// { id: 1, name: 'Bob', email: 'bob@example.com' }

// Using computed properties in literal
const status = 'active';
const config = {
    [`is${status.charAt(0).toUpperCase() + status.slice(1)}`]: true
};
// { isActive: true }

// Symbol as computed property
const SECRET = Symbol('secret');
const obj = {
    public: 'visible',
    [SECRET]: 'hidden'  // Property name is a symbol
};
console.log(obj.public);  // 'visible'
console.log(obj[SECRET]);  // 'hidden'
console.log(Object.keys(obj));  // ['public'] - symbols not enumerable

// Spread properties (ES2018)
const defaults = { theme: 'dark', lang: 'en' };
const userPrefs = { lang: 'fr', fontSize: 14 };
const settings = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'fr', fontSize: 14 }
// userPrefs.lang overwrites defaults.lang

2. Property Descriptors and Object.defineProperty

Descriptor Attribute Type Default (defineProperty) Default (Literal) Description
value any undefined property value The property's value (data descriptor)
writable boolean false true Can property value be changed?
enumerable boolean false true Shows in for...in, Object.keys()?
configurable boolean false true Can descriptor be changed? Can property be deleted?
get function undefined N/A Getter function (accessor descriptor)
set function undefined N/A Setter function (accessor descriptor)
Method Syntax Description
Object.defineProperty() Object.defineProperty(obj, prop, descriptor) Define or modify single property with full control over attributes
Object.defineProperties() Object.defineProperties(obj, descriptors) Define or modify multiple properties at once
Object.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor(obj, prop) Get descriptor object for single property
Object.getOwnPropertyDescriptors() ES2017 Object.getOwnPropertyDescriptors(obj) Get descriptors for all own properties

Example: Property descriptors in action

const obj = {};

// Define property with custom descriptor
Object.defineProperty(obj, 'name', {
    value: 'Alice',
    writable: false,     // Cannot change value
    enumerable: true,    // Shows in for...in, Object.keys()
    configurable: false  // Cannot delete or reconfigure
});

obj.name = 'Bob';  // Fails silently (strict mode: TypeError)
console.log(obj.name);  // 'Alice' (unchanged)

delete obj.name;  // Fails silently (configurable: false)
console.log(obj.name);  // 'Alice' (still exists)

// Check property descriptor
const desc = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(desc);
// { value: 'Alice', writable: false, enumerable: true, configurable: false }

// Compare with literal property (all attributes true)
const obj2 = { name: 'Charlie' };
const desc2 = Object.getOwnPropertyDescriptor(obj2, 'name');
console.log(desc2);
// { value: 'Charlie', writable: true, enumerable: true, configurable: true }

Example: Accessor properties with get/set

const person = {
    firstName: 'John',
    lastName: 'Doe'
};

// Define computed property with getter/setter
Object.defineProperty(person, 'fullName', {
    get() {
        return `${this.firstName} ${this.lastName}`;
    },
    set(value) {
        [this.firstName, this.lastName] = value.split(' ');
    },
    enumerable: true,
    configurable: true
});

console.log(person.fullName);  // 'John Doe'
person.fullName = 'Jane Smith';
console.log(person.firstName);  // 'Jane'
console.log(person.lastName);  // 'Smith'

// Define multiple properties at once
const account = {};
Object.defineProperties(account, {
    _balance: {
        value: 0,
        writable: true,
        enumerable: false  // Private-like (convention)
    },
    balance: {
        get() { return this._balance; },
        set(val) {
            if (val < 0) throw new Error('Balance cannot be negative');
            this._balance = val;
        },
        enumerable: true
    },
    currency: {
        value: 'USD',
        writable: false,
        enumerable: true
    }
});

console.log(account.balance);  // 0
account.balance = 100;
console.log(account.balance);  // 100
// account.balance = -50;  // Error: Balance cannot be negative

Example: Non-enumerable and non-configurable properties

const obj = { a: 1, b: 2 };

// Add non-enumerable property
Object.defineProperty(obj, 'hidden', {
    value: 'secret',
    enumerable: false  // Won't show in iteration
});

console.log(obj.hidden);  // 'secret' (accessible directly)
console.log(Object.keys(obj));  // ['a', 'b'] (hidden not listed)
for (let key in obj) {
    console.log(key);  // Only logs 'a' and 'b'
}

// Non-configurable property
Object.defineProperty(obj, 'permanent', {
    value: 'cannot change',
    configurable: false
});

// Cannot reconfigure
try {
    Object.defineProperty(obj, 'permanent', {
        value: 'new value'  // TypeError: cannot redefine
    });
} catch (e) {
    console.log('Cannot reconfigure');
}

// Cannot delete
delete obj.permanent;  // Fails silently
console.log(obj.permanent);  // Still exists

// Get all descriptors (ES2017)
const allDescriptors = Object.getOwnPropertyDescriptors(obj);
console.log(allDescriptors.hidden);
// { value: 'secret', writable: false, enumerable: false, configurable: false }
Important: Data descriptors (value/writable) and accessor descriptors (get/set) are mutually exclusive. A property can have value OR get/set, not both. Non-configurable properties cannot be deleted or have their descriptor changed (except writable can go from true to false).

3. Object Methods (Object.keys, Object.values, Object.entries)

Method Returns ES Version Description
Object.keys(obj) Array of strings ES5 Own enumerable property names (keys)
Object.values(obj) ES2017 Array of values ES2017 Own enumerable property values
Object.entries(obj) ES2017 Array of [key, value] pairs ES2017 Own enumerable properties as key-value pairs
Object.fromEntries(entries) ES2019 Object ES2019 Create object from array of [key, value] pairs (inverse of entries)
Object.getOwnPropertyNames(obj) Array of strings ES5 All own property names (including non-enumerable)
Object.getOwnPropertySymbols(obj) ES6 Array of symbols ES6 All own symbol properties
Object.hasOwn(obj, prop) ES2022 boolean ES2022 Safe alternative to hasOwnProperty (recommended)
obj.hasOwnProperty(prop) boolean ES3 Check if property is own (not inherited); use Object.hasOwn instead
Comparison for...in Object.keys() Object.getOwnPropertyNames()
Own Properties ✓ Yes ✓ Yes ✓ Yes
Inherited Properties ✓ Yes (from prototype chain) ✗ No ✗ No
Non-enumerable ✗ No ✗ No ✓ Yes
Symbol Properties ✗ No ✗ No ✗ No (use getOwnPropertySymbols)

Example: Object.keys, values, entries

const user = {
    id: 1,
    name: 'Alice',
    email: 'alice@example.com',
    active: true
};

// Get all keys
const keys = Object.keys(user);
console.log(keys);  // ['id', 'name', 'email', 'active']

// Get all values (ES2017)
const values = Object.values(user);
console.log(values);  // [1, 'Alice', 'alice@example.com', true]

// Get key-value pairs (ES2017)
const entries = Object.entries(user);
console.log(entries);
// [['id', 1], ['name', 'Alice'], ['email', 'alice@example.com'], ['active', true]]

// Iterate over entries
Object.entries(user).forEach(([key, value]) => {
    console.log(`${key}: ${value}`);
});

// Transform object using entries
const doubled = Object.fromEntries(
    Object.entries({ a: 1, b: 2, c: 3 })
        .map(([key, value]) => [key, value * 2])
);
console.log(doubled);  // { a: 2, b: 4, c: 6 }

// Filter object properties
const activeUsers = Object.fromEntries(
    Object.entries({ u1: { active: true }, u2: { active: false } })
        .filter(([key, val]) => val.active)
);
// { u1: { active: true } }

Example: Enumerable vs non-enumerable properties

const obj = { a: 1, b: 2 };

// Add non-enumerable property
Object.defineProperty(obj, 'hidden', {
    value: 'secret',
    enumerable: false
});

// Add symbol property
const sym = Symbol('symbol');
obj[sym] = 'symbol value';

console.log(Object.keys(obj));  // ['a', 'b'] (only enumerable string keys)
console.log(Object.getOwnPropertyNames(obj));  // ['a', 'b', 'hidden'] (includes non-enumerable)
console.log(Object.getOwnPropertySymbols(obj));  // [Symbol(symbol)]

// for...in includes inherited enumerable properties
function Parent() {}
Parent.prototype.inherited = 'from parent';

const child = Object.create(new Parent());
child.own = 'own property';

for (let key in child) {
    console.log(key);  // Logs: 'own', 'inherited'
}

// Filter to own properties only
for (let key in child) {
    if (Object.hasOwn(child, key)) {  // ES2022
        console.log(key);  // Logs: 'own' only
    }
}

// Alternative: hasOwnProperty (older)
for (let key in child) {
    if (child.hasOwnProperty(key)) {
        console.log(key);  // Logs: 'own' only
    }
}

Example: Practical use cases

// Count object properties
const obj = { a: 1, b: 2, c: 3 };
const count = Object.keys(obj).length;  // 3

// Check if object is empty
const isEmpty = Object.keys(obj).length === 0;

// Merge objects by converting to entries
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = Object.fromEntries([
    ...Object.entries(obj1),
    ...Object.entries(obj2)
]);
// { a: 1, b: 3, c: 4 } (obj2 overwrites obj1)

// Convert object to Map
const map = new Map(Object.entries(obj));

// Convert Map to object
const objFromMap = Object.fromEntries(map);

// Swap keys and values
const original = { a: 1, b: 2, c: 3 };
const swapped = Object.fromEntries(
    Object.entries(original).map(([k, v]) => [v, k])
);
// { 1: 'a', 2: 'b', 3: 'c' }

// Pick specific properties
function pick(obj, keys) {
    return Object.fromEntries(
        keys.filter(key => key in obj)
            .map(key => [key, obj[key]])
    );
}
const user = { id: 1, name: 'Alice', email: 'a@example.com', age: 30 };
const userPreview = pick(user, ['id', 'name']);
// { id: 1, name: 'Alice' }

4. Object Creation Patterns (Object.create, constructors)

Pattern Syntax Prototype Use Case
Object Literal const obj = { } Object.prototype Simple data objects, quick creation
Object() Constructor const obj = new Object() Object.prototype Rarely used (literal preferred)
Object.create() ES5 Object.create(proto, descriptors?) Specified proto Prototypal inheritance, null prototype objects
Constructor Function function Ctor() { } + new Ctor() Ctor.prototype Pre-ES6 classes, instance creation with shared methods
Factory Function function create() { return { }; } Object.prototype (or custom) Object creation without 'new', private state
Class Syntax ES6 class C { } + new C() C.prototype Modern OOP, cleaner syntax over constructors
Method Description Returns
Object.create(proto) Create new object with specified prototype New object inheriting from proto
Object.create(proto, descriptors) Create object with prototype and property descriptors New object with specified properties
Object.create(null) Create object with no prototype (truly empty) Object without any inherited properties
Object.setPrototypeOf(obj, proto) Change prototype of existing object (slow, avoid) The modified object
Object.getPrototypeOf(obj) Get prototype of object Prototype object or null

Example: Object.create() patterns

// Create object with specific prototype
const personProto = {
    greet() {
        return `Hello, I'm ${this.name}`;
    }
};

const person = Object.create(personProto);
person.name = 'Alice';
console.log(person.greet());  // "Hello, I'm Alice"
console.log(Object.getPrototypeOf(person) === personProto);  // true

// Create with property descriptors
const user = Object.create(personProto, {
    name: {
        value: 'Bob',
        writable: true,
        enumerable: true
    },
    age: {
        value: 30,
        writable: true,
        enumerable: true
    }
});
console.log(user.name);  // 'Bob'
console.log(user.greet());  // "Hello, I'm Bob"

// Create object with null prototype (no inherited properties)
const dict = Object.create(null);
dict.key = 'value';
console.log(dict.toString);  // undefined (no inherited methods)
console.log(dict.hasOwnProperty);  // undefined
// Useful for pure data storage, no prototype pollution

// Compare with literal
const literal = {};
console.log(literal.toString);  // [Function: toString] (inherited)
console.log(Object.getPrototypeOf(literal) === Object.prototype);  // true

Example: Constructor functions

// Constructor function (pre-ES6 classes)
function Person(name, age) {
    // Instance properties (unique per instance)
    this.name = name;
    this.age = age;
}

// Shared methods on prototype (memory efficient)
Person.prototype.greet = function() {
    return `Hello, I'm ${this.name}`;
};

Person.prototype.getAge = function() {
    return this.age;
};

// Create instances with 'new'
const alice = new Person('Alice', 30);
const bob = new Person('Bob', 25);

console.log(alice.greet());  // "Hello, I'm Alice"
console.log(bob.greet());  // "Hello, I'm Bob"
console.log(alice.greet === bob.greet);  // true (shared method)

// Verify prototype
console.log(Object.getPrototypeOf(alice) === Person.prototype);  // true
console.log(alice instanceof Person);  // true

// Forgetting 'new' - common mistake
function SafeConstructor(name) {
    // Guard against missing 'new'
    if (!(this instanceof SafeConstructor)) {
        return new SafeConstructor(name);
    }
    this.name = name;
}

const instance1 = new SafeConstructor('Alice');
const instance2 = SafeConstructor('Bob');  // Works without 'new'
console.log(instance2 instanceof SafeConstructor);  // true

Example: Factory functions

// Factory function - no 'new' needed
function createPerson(name, age) {
    // Private variables (closure)
    let privateData = 'secret';
    
    // Return object with public interface
    return {
        name,
        age,
        greet() {
            return `Hello, I'm ${this.name}`;
        },
        getPrivate() {
            return privateData;  // Access private via closure
        }
    };
}

const person1 = createPerson('Alice', 30);  // No 'new'
const person2 = createPerson('Bob', 25);

console.log(person1.greet());  // "Hello, I'm Alice"
console.log(person1.getPrivate());  // 'secret'
console.log(person1.privateData);  // undefined (truly private)

// Factory with prototypal inheritance
function createUser(name) {
    const proto = {
        greet() { return `Hello ${this.name}`; }
    };
    
    const user = Object.create(proto);
    user.name = name;
    return user;
}

const user = createUser('Charlie');
console.log(user.greet());  // "Hello Charlie"

// Modern: ES6 class (preferred over constructors)
class PersonClass {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        return `Hello, I'm ${this.name}`;
    }
}

const personInstance = new PersonClass('Dave', 35);
console.log(personInstance.greet());  // "Hello, I'm Dave"
Best Practice: Modern JavaScript prefers ES6 classes over constructor functions for clarity. Use factory functions when you need private state via closures or want to avoid new. Use Object.create(null) for dictionaries/maps to avoid prototype pollution. Avoid Object.setPrototypeOf() as it's very slow.

5. Object Cloning and Merging (Object.assign, spread)

Method Syntax Copy Type Nested Objects
Object.assign() ES6 Object.assign(target, ...sources) Shallow copy Copies references (not deep)
Spread Operator ES2018 { ...obj } Shallow copy Copies references (not deep)
JSON.parse/stringify JSON.parse(JSON.stringify(obj)) Deep copy ✓ Clones nested, but loses functions/symbols/dates
structuredClone() NEW structuredClone(obj) Deep copy ✓ Clones nested, preserves types (not functions)
Manual Deep Clone Recursive function Deep copy ✓ Full control, handle all types
Feature Object.assign() Spread {...} Differences
Copy enumerable props ✓ Yes ✓ Yes Both copy own enumerable properties
Trigger setters ✓ Yes ✗ No assign() invokes setters, spread defines new props
Copy getters/setters ✗ No (executes getter) ✗ No (executes getter) Both execute getters, copy resulting values
Mutate target ✓ Yes (modifies first arg) ✗ No (creates new object) assign() mutates, spread creates new
Syntax Function call Operator (concise) Spread is more concise and readable

Example: Shallow copying with Object.assign and spread

const original = {
    name: 'Alice',
    age: 30,
    address: { city: 'NYC', zip: '10001' }
};

// Object.assign - shallow copy
const copy1 = Object.assign({}, original);
copy1.name = 'Bob';
copy1.address.city = 'LA';  // Modifies nested object

console.log(original.name);  // 'Alice' (primitive copied)
console.log(original.address.city);  // 'LA' (nested reference shared!)

// Spread operator - shallow copy (ES2018)
const copy2 = { ...original };
copy2.age = 25;
copy2.address.zip = '90001';  // Also modifies original!

console.log(original.age);  // 30 (primitive copied)
console.log(original.address.zip);  // '90001' (nested reference shared!)

// Merge multiple objects
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const obj3 = { c: 5, d: 6 };

// Using Object.assign
const merged1 = Object.assign({}, obj1, obj2, obj3);
// { a: 1, b: 3, c: 5, d: 6 } (later values overwrite)

// Using spread (more common)
const merged2 = { ...obj1, ...obj2, ...obj3 };
// { a: 1, b: 3, c: 5, d: 6 } (same result)

// Add/override properties while spreading
const enhanced = {
    ...original,
    age: 35,  // Override
    email: 'alice@example.com'  // Add new
};
// { name: 'Alice', age: 35, address: {...}, email: '...' }

Example: Deep cloning techniques

const original = {
    name: 'Alice',
    age: 30,
    hobbies: ['reading', 'coding'],
    address: { city: 'NYC', coords: { lat: 40, lng: -74 } },
    createdAt: new Date('2024-01-01')
};

// Method 1: JSON (quick but lossy)
const jsonClone = JSON.parse(JSON.stringify(original));
jsonClone.hobbies.push('gaming');
jsonClone.address.city = 'LA';

console.log(original.hobbies);  // ['reading', 'coding'] (not affected)
console.log(original.address.city);  // 'NYC' (not affected)
console.log(jsonClone.createdAt);  // String, not Date! (limitation)

// Method 2: structuredClone() (modern, recommended)
const structClone = structuredClone(original);
structClone.address.coords.lat = 35;

console.log(original.address.coords.lat);  // 40 (not affected)
console.log(structClone.createdAt instanceof Date);  // true (preserved!)

// Limitations: cannot clone functions
const withFunction = { 
    data: 'value', 
    method() { return this.data; } 
};
// structuredClone(withFunction);  // Error: functions not cloneable

// Method 3: Manual deep clone (full control)
function deepClone(obj, seen = new WeakMap()) {
    // Handle primitives and null
    if (obj === null || typeof obj !== 'object') return obj;
    
    // Handle circular references
    if (seen.has(obj)) return seen.get(obj);
    
    // Handle Date
    if (obj instanceof Date) return new Date(obj);
    
    // Handle Array
    if (Array.isArray(obj)) {
        const arrCopy = [];
        seen.set(obj, arrCopy);
        obj.forEach((item, i) => {
            arrCopy[i] = deepClone(item, seen);
        });
        return arrCopy;
    }
    
    // Handle Object
    const objCopy = {};
    seen.set(obj, objCopy);
    Object.keys(obj).forEach(key => {
        objCopy[key] = deepClone(obj[key], seen);
    });
    return objCopy;
}

const manualClone = deepClone(original);
manualClone.address.coords.lng = -118;
console.log(original.address.coords.lng);  // -74 (not affected)

Example: Object.assign vs spread differences

// Difference 1: Mutating vs creating
const target = { a: 1 };
const source = { b: 2 };

Object.assign(target, source);  // Mutates target
console.log(target);  // { a: 1, b: 2 } (modified)

const newObj = { ...target, ...source };  // Creates new object
// target unchanged

// Difference 2: Setters
const obj = {
    _value: 0,
    set value(v) {
        console.log('Setter called:', v);
        this._value = v;
    }
};

// Object.assign triggers setter
Object.assign(obj, { value: 10 });  // Logs: "Setter called: 10"

// Spread defines new property (doesn't trigger setter)
const copy = { ...obj, value: 20 };  // No setter call
console.log(copy.value);  // 20 (simple property, not setter)

// Use case: Default options
function configure(options = {}) {
    const defaults = {
        timeout: 5000,
        retries: 3,
        cache: true
    };
    
    // Merge with user options (spread preferred)
    return { ...defaults, ...options };
}

const config1 = configure({ timeout: 10000 });
// { timeout: 10000, retries: 3, cache: true }

const config2 = configure({ retries: 5, debug: true });
// { timeout: 5000, retries: 5, cache: true, debug: true }
Warning: Both Object.assign() and spread create shallow copies. Nested objects are copied by reference, not value. Modifying nested properties affects the original. For true deep copies, use structuredClone() (modern) or implement custom deep clone logic.

6. Property Getters and Setters

Syntax Form Example When to Use
Object Literal Getter { get prop() { return value; } } Computed properties, validation on read
Object Literal Setter { set prop(val) { this._prop = val; } } Validation/transformation on write
defineProperty Getter Object.defineProperty(obj, 'prop', { get() {} }) Add getter to existing object
defineProperty Setter Object.defineProperty(obj, 'prop', { set(v) {} }) Add setter to existing object
Class Getter class C { get prop() { } } ES6 class computed properties
Class Setter class C { set prop(v) { } } ES6 class property validation
Use Case Pattern Example
Computed Property Getter derives value from other properties get fullName() { return this.first + ' ' + this.last; }
Validation Setter validates before assignment set age(v) { if (v < 0) throw Error; this._age = v; }
Lazy Initialization Getter computes value on first access get data() { return this._data || (this._data = load()); }
Side Effects Setter triggers additional actions set value(v) { this._value = v; this.notify(); }
Read-only Property Getter without setter get id() { return this._id; } (no setter)
Private Backing Field Store value in _property, expose via getter/setter get name() { return this._name; }

Example: Getters and setters in object literals

const person = {
    firstName: 'John',
    lastName: 'Doe',
    _age: 30,  // Backing field (convention: underscore = private-like)
    
    // Getter: computed property
    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    },
    
    // Setter: parse and set multiple properties
    set fullName(name) {
        const parts = name.split(' ');
        this.firstName = parts[0];
        this.lastName = parts[1];
    },
    
    // Getter with validation/transformation
    get age() {
        return this._age;
    },
    
    // Setter with validation
    set age(value) {
        if (typeof value !== 'number' || value < 0 || value > 150) {
            throw new Error('Invalid age');
        }
        this._age = value;
    },
    
    // Read-only property (getter only)
    get birthYear() {
        return new Date().getFullYear() - this._age;
    }
};

// Using getters and setters
console.log(person.fullName);  // "John Doe" (getter called)
person.fullName = 'Jane Smith';  // Setter called
console.log(person.firstName);  // 'Jane'
console.log(person.lastName);  // 'Smith'

console.log(person.age);  // 30 (getter)
person.age = 25;  // Setter with validation
// person.age = -5;  // Error: Invalid age

console.log(person.birthYear);  // Computed from age
// person.birthYear = 1990;  // TypeError: no setter (read-only)

Example: Getters/setters in classes

class Rectangle {
    constructor(width, height) {
        this._width = width;
        this._height = height;
    }
    
    // Getter for computed property
    get area() {
        return this._width * this._height;
    }
    
    get perimeter() {
        return 2 * (this._width + this._height);
    }
    
    // Getter/setter with validation
    get width() {
        return this._width;
    }
    
    set width(value) {
        if (value <= 0) {
            throw new Error('Width must be positive');
        }
        this._width = value;
    }
    
    get height() {
        return this._height;
    }
    
    set height(value) {
        if (value <= 0) {
            throw new Error('Height must be positive');
        }
        this._height = value;
    }
}

const rect = new Rectangle(10, 5);
console.log(rect.area);  // 50 (computed)
console.log(rect.perimeter);  // 30 (computed)

rect.width = 20;  // Setter with validation
console.log(rect.area);  // 100 (auto-updated)

// rect.width = -5;  // Error: Width must be positive
// rect.area = 200;  // TypeError: no setter (read-only)

Example: Advanced getter/setter patterns

// Lazy initialization with getter
class DataLoader {
    constructor(url) {
        this.url = url;
        this._data = null;
    }
    
    get data() {
        // Load data on first access, cache for subsequent access
        if (this._data === null) {
            console.log('Loading data...');
            this._data = this.loadData();  // Expensive operation
        }
        return this._data;
    }
    
    loadData() {
        // Simulate data loading
        return { loaded: true, url: this.url };
    }
}

const loader = new DataLoader('/api/data');
console.log(loader.data);  // Logs: "Loading data...", returns data
console.log(loader.data);  // No log, returns cached data

// Observable pattern with setters
class Observable {
    constructor() {
        this._observers = [];
        this._value = null;
    }
    
    get value() {
        return this._value;
    }
    
    set value(newValue) {
        const oldValue = this._value;
        this._value = newValue;
        this.notify(oldValue, newValue);
    }
    
    subscribe(observer) {
        this._observers.push(observer);
    }
    
    notify(oldValue, newValue) {
        this._observers.forEach(fn => fn(newValue, oldValue));
    }
}

const obs = new Observable();
obs.subscribe((newVal, oldVal) => {
    console.log(`Changed from ${oldVal} to ${newVal}`);
});
obs.value = 10;  // Logs: "Changed from null to 10"
obs.value = 20;  // Logs: "Changed from 10 to 20"

// Type conversion in setter
const config = {
    _port: 8080,
    
    get port() {
        return this._port;
    },
    
    set port(value) {
        // Always store as number
        this._port = Number(value);
    }
};

config.port = '3000';  // String input
console.log(config.port);  // 3000 (number)
console.log(typeof config.port);  // 'number'
Best Practice: Use getters for computed properties and derived state. Use setters for validation, type conversion, and triggering side effects. Follow naming convention: use underscore prefix for backing fields (e.g., _value). For true privacy in modern JS, use private fields (#field) instead of convention.

7. Object Freezing and Sealing (freeze, seal, preventExtensions)

Method Add Props Delete Props Modify Values Change Config
Normal Object ✓ Yes ✓ Yes ✓ Yes ✓ Yes
Object.preventExtensions() ✗ No ✓ Yes ✓ Yes ✓ Yes
Object.seal() ✗ No ✗ No ✓ Yes ✗ No
Object.freeze() ✗ No ✗ No ✗ No ✗ No
Method Effect Check Method Use Case
Object.preventExtensions(obj) Cannot add new properties Object.isExtensible(obj) Prevent accidental property additions
Object.seal(obj) Cannot add/delete, can modify existing Object.isSealed(obj) Fixed property set, mutable values
Object.freeze(obj) Completely immutable (shallow) Object.isFrozen(obj) Constants, immutable config

Example: Object.preventExtensions()

const obj = { a: 1, b: 2 };

// Prevent adding new properties
Object.preventExtensions(obj);

// Existing properties work normally
obj.a = 10;  // ✓ Works (can modify)
delete obj.b;  // ✓ Works (can delete)
console.log(obj);  // { a: 10 }

// Cannot add new properties
obj.c = 3;  // ✗ Fails silently (strict mode: TypeError)
console.log(obj.c);  // undefined

// Check if extensible
console.log(Object.isExtensible(obj));  // false

// Cannot make extensible again (one-way operation)
// No Object.makeExtensible() method

Example: Object.seal()

const user = { name: 'Alice', age: 30 };

// Seal object - fixed property set
Object.seal(user);

// Can modify existing properties
user.name = 'Bob';  // ✓ Works
user.age = 35;  // ✓ Works
console.log(user);  // { name: 'Bob', age: 35 }

// Cannot add new properties
user.email = 'test@example.com';  // ✗ Fails silently
console.log(user.email);  // undefined

// Cannot delete properties
delete user.age;  // ✗ Fails silently
console.log(user.age);  // 35 (still exists)

// Cannot reconfigure property descriptors
// Object.defineProperty(user, 'name', { enumerable: false });  // ✗ TypeError

// Check if sealed
console.log(Object.isSealed(user));  // true

// Sealed implies non-extensible
console.log(Object.isExtensible(user));  // false

Example: Object.freeze()

const config = {
    API_URL: 'https://api.example.com',
    TIMEOUT: 5000,
    MAX_RETRIES: 3
};

// Freeze object - completely immutable
Object.freeze(config);

// Cannot modify properties
config.TIMEOUT = 10000;  // ✗ Fails silently (strict mode: TypeError)
console.log(config.TIMEOUT);  // 5000 (unchanged)

// Cannot add properties
config.DEBUG = true;  // ✗ Fails silently
console.log(config.DEBUG);  // undefined

// Cannot delete properties
delete config.MAX_RETRIES;  // ✗ Fails silently
console.log(config.MAX_RETRIES);  // 3 (still exists)

// Check if frozen
console.log(Object.isFrozen(config));  // true

// Frozen implies sealed and non-extensible
console.log(Object.isSealed(config));  // true
console.log(Object.isExtensible(config));  // false

// Common use: constants
const CONSTANTS = Object.freeze({
    PI: 3.14159,
    E: 2.71828,
    MAX_INT: 2147483647
});
// CONSTANTS.PI = 3.14;  // ✗ Cannot modify

Example: Shallow vs deep freeze

// Object.freeze is SHALLOW - nested objects not frozen
const user = {
    name: 'Alice',
    settings: {
        theme: 'dark',
        notifications: true
    }
};

Object.freeze(user);

// Top-level frozen
user.name = 'Bob';  // ✗ Fails
console.log(user.name);  // 'Alice' (unchanged)

// Nested object NOT frozen
user.settings.theme = 'light';  // ✓ Works!
user.settings.notifications = false;  // ✓ Works!
console.log(user.settings.theme);  // 'light' (modified)

// Deep freeze implementation
function deepFreeze(obj) {
    // Freeze object itself
    Object.freeze(obj);
    
    // Recursively freeze all object properties
    Object.getOwnPropertyNames(obj).forEach(prop => {
        const value = obj[prop];
        if (value && typeof value === 'object' && !Object.isFrozen(value)) {
            deepFreeze(value);
        }
    });
    
    return obj;
}

const deepUser = {
    name: 'Charlie',
    settings: { theme: 'dark', notifications: true }
};

deepFreeze(deepUser);

// Now nested is also frozen
deepUser.settings.theme = 'light';  // ✗ Fails
console.log(deepUser.settings.theme);  // 'dark' (unchanged)

Example: Practical use cases

// Use case 1: Enum-like constants
const Status = Object.freeze({
    PENDING: 'pending',
    APPROVED: 'approved',
    REJECTED: 'rejected'
});
// Status.PENDING = 'other';  // ✗ Cannot modify

// Use case 2: Configuration objects
const appConfig = Object.freeze({
    version: '1.0.0',
    apiEndpoint: '/api/v1',
    features: {
        auth: true,
        analytics: true
    }
});

// Use case 3: Prevent accidental mutations in function
function processUser(user) {
    // Prevent function from modifying passed object
    const immutableUser = Object.freeze({ ...user });
    
    // immutableUser.name = 'Modified';  // ✗ Fails
    
    // Return new object instead
    return { ...immutableUser, processed: true };
}

// Use case 4: Sealed object with mutable values
const counters = Object.seal({
    clicks: 0,
    views: 0,
    purchases: 0
});

// Can update counters
counters.clicks++;
counters.views += 10;

// But cannot add new counters
// counters.downloads = 0;  // ✗ Fails

// Comparison matrix
const obj1 = { a: 1 };
Object.preventExtensions(obj1);
console.log({
    canAdd: !Object.isExtensible(obj1),      // true (cannot add)
    canDelete: !Object.isSealed(obj1),        // true (can delete)
    canModify: !Object.isFrozen(obj1)         // true (can modify)
});

const obj2 = { a: 1 };
Object.seal(obj2);
console.log({
    canAdd: !Object.isExtensible(obj2),      // true (cannot add)
    canDelete: !Object.isSealed(obj2),        // false (cannot delete)
    canModify: !Object.isFrozen(obj2)         // true (can modify)
});

const obj3 = { a: 1 };
Object.freeze(obj3);
console.log({
    canAdd: !Object.isExtensible(obj3),      // true (cannot add)
    canDelete: !Object.isSealed(obj3),        // false (cannot delete)
    canModify: !Object.isFrozen(obj3)         // false (cannot modify)
});
Important Limitations:
  • Shallow only: All three methods (preventExtensions, seal, freeze) only affect the immediate object, not nested objects
  • Irreversible: Cannot undo these operations - once frozen/sealed, always frozen/sealed
  • Silent failures: In non-strict mode, violations fail silently (no error thrown)
  • Performance: Frozen objects may have optimization benefits but are rarely the bottleneck

Section 7 Summary

  • Modern literals: Property/method shorthand, computed properties, spread syntax, getters/setters for clean object creation
  • Property descriptors: Control writable/enumerable/configurable attributes; defineProperty for fine-grained control
  • Object methods: keys/values/entries for iteration; fromEntries for transformation; hasOwn for safe property checks
  • Creation patterns: Object.create for prototypal inheritance; constructors (legacy) vs classes (modern); factory functions for flexibility
  • Cloning: Spread/assign for shallow copy; structuredClone for deep copy; JSON for simple deep copy (lossy)
  • Getters/setters: Computed properties, validation, lazy initialization; use _prefix convention for backing fields
  • Immutability: preventExtensions (no add), seal (no add/delete), freeze (no changes); all shallow, implement deepFreeze for nested