🎯 Objects

Key-Value Data Structures

Objects in JavaScript

Objects are collections of key-value pairs. They're the foundation of JavaScript's data structures and object-oriented programming.

📝 Creating Objects

// Object literal (most common)
let person = {
    name: 'John',
    age: 30,
    city: 'New York'
};

// Empty object
let obj1 = {};
let obj2 = new Object();

// With computed property names
let key = 'email';
let user = {
    name: 'Jane',
    [key]: 'jane@example.com',
    ['is' + 'Active']: true
};
console.log(user);  // { name: 'Jane', email: 'jane@example.com', isActive: true }

// Property shorthand (ES6)
let name = 'Bob';
let age = 25;
let person2 = { name, age };  // Same as { name: name, age: age }

// Method shorthand
let calculator = {
    // Old way
    add: function(a, b) {
        return a + b;
    },
    // New way (ES6)
    subtract(a, b) {
        return a - b;
    }
};

// Object.create()
let proto = { greet() { return 'Hello'; } };
let obj3 = Object.create(proto);
console.log(obj3.greet());  // 'Hello'

🔍 Accessing Properties

let person = {
    name: 'John',
    age: 30,
    'favorite color': 'blue',
    address: {
        street: '123 Main St',
        city: 'New York'
    }
};

// Dot notation
console.log(person.name);  // 'John'
console.log(person.age);   // 30

// Bracket notation
console.log(person['name']);           // 'John'
console.log(person['favorite color']); // 'blue' (required for spaces)

// Dynamic property access
let prop = 'age';
console.log(person[prop]);  // 30

// Nested properties
console.log(person.address.city);        // 'New York'
console.log(person['address']['city']);  // 'New York'

// Optional chaining (ES2020)
let user = null;
console.log(user?.name);  // undefined (no error)

let person2 = { name: 'Jane' };
console.log(person2?.address?.city);  // undefined (no error)

// Non-existent property
console.log(person.email);  // undefined

✏️ Modifying Objects

let person = {
    name: 'John',
    age: 30
};

// Add property
person.email = 'john@example.com';
person['phone'] = '555-1234';

// Modify property
person.age = 31;
person['name'] = 'John Doe';

// Delete property
delete person.phone;
console.log(person);  // { name: 'John Doe', age: 31, email: '...' }

// Check if property exists
console.log('email' in person);  // true
console.log('phone' in person);  // false

// hasOwnProperty (own vs inherited)
console.log(person.hasOwnProperty('name'));      // true
console.log(person.hasOwnProperty('toString'));  // false (inherited)

// Object.hasOwn (ES2022 - safer)
console.log(Object.hasOwn(person, 'name'));  // true

🔄 Iterating Over Objects

let person = {
    name: 'John',
    age: 30,
    city: 'New York'
};

// for...in loop
for (let key in person) {
    console.log(`${key}: ${person[key]}`);
}

// Object.keys() - returns array of keys
let keys = Object.keys(person);
console.log(keys);  // ['name', 'age', 'city']

keys.forEach(key => {
    console.log(`${key}: ${person[key]}`);
});

// Object.values() - returns array of values (ES2017)
let values = Object.values(person);
console.log(values);  // ['John', 30, 'New York']

// Object.entries() - returns array of [key, value] pairs (ES2017)
let entries = Object.entries(person);
console.log(entries);  // [['name', 'John'], ['age', 30], ['city', 'New York']]

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

// Convert back to object
let obj = Object.fromEntries(entries);
console.log(obj);  // { name: 'John', age: 30, city: 'New York' }

📦 Object Methods

Object.assign

// Copy/merge objects
let target = { a: 1, b: 2 };
let source1 = { b: 3, c: 4 };
let source2 = { c: 5, d: 6 };

let result = Object.assign(target, source1, source2);
console.log(result);  // { a: 1, b: 3, c: 5, d: 6 }
console.log(target);  // target is modified!

// Clone object (shallow)
let person = { name: 'John', age: 30 };
let clone = Object.assign({}, person);

// Add properties while cloning
let extended = Object.assign({}, person, { city: 'New York' });

// Modern alternative: spread operator
let clone2 = { ...person };
let extended2 = { ...person, city: 'New York' };

Object.freeze and Object.seal

// freeze - make immutable
let person = { name: 'John', age: 30 };
Object.freeze(person);

person.age = 31;        // Silently fails (error in strict mode)
person.city = 'NY';     // Silently fails
delete person.name;     // Silently fails
console.log(person);    // { name: 'John', age: 30 } unchanged

console.log(Object.isFrozen(person));  // true

// Note: shallow freeze only
let user = { name: 'Jane', address: { city: 'Boston' } };
Object.freeze(user);
// user.name = 'John';  // Fails
user.address.city = 'NYC';  // Works! (nested object not frozen)

// seal - prevent add/delete, allow modify
let obj = { a: 1, b: 2 };
Object.seal(obj);

obj.a = 10;      // OK - can modify
obj.c = 3;       // Fails - can't add
delete obj.b;    // Fails - can't delete

console.log(Object.isSealed(obj));  // true

// preventExtensions - allow delete/modify, prevent add
let obj2 = { x: 1 };
Object.preventExtensions(obj2);

obj2.x = 10;     // OK
delete obj2.x;   // OK
obj2.y = 2;      // Fails

console.log(Object.isExtensible(obj2));  // false

Object.defineProperty

// Define property with descriptors
let person = {};

Object.defineProperty(person, 'name', {
    value: 'John',
    writable: true,      // Can be changed
    enumerable: true,    // Shows in for...in
    configurable: true   // Can be deleted
});

// Getter and setter
Object.defineProperty(person, 'age', {
    value: 30,
    writable: false  // Read-only
});

// person.age = 31;  // Fails

// With getter/setter
Object.defineProperty(person, 'fullName', {
    get() {
        return `${this.firstName} ${this.lastName}`;
    },
    set(value) {
        [this.firstName, this.lastName] = value.split(' ');
    }
});

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

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

// defineProperties - multiple at once
Object.defineProperties(person, {
    email: {
        value: 'john@example.com',
        writable: true
    },
    phone: {
        value: '555-1234',
        writable: false
    }
});

🔗 this Keyword

// this refers to the object the method belongs to
let person = {
    name: 'John',
    age: 30,
    greet() {
        console.log(`Hi, I'm ${this.name}`);
    },
    getAge() {
        return this.age;
    }
};

person.greet();  // 'Hi, I'm John'

// Arrow functions don't have their own this
let person2 = {
    name: 'Jane',
    greet: () => {
        console.log(this.name);  // undefined (this is from outer scope)
    }
};

// Nested functions lose this context
let person3 = {
    name: 'Bob',
    friends: ['Alice', 'Charlie'],
    showFriends() {
        this.friends.forEach(function(friend) {
            // console.log(`${this.name} knows ${friend}`);  // Error!
        });
    }
};

// Solution 1: Arrow function
let person4 = {
    name: 'Bob',
    friends: ['Alice', 'Charlie'],
    showFriends() {
        this.friends.forEach(friend => {
            console.log(`${this.name} knows ${friend}`);  // Works!
        });
    }
};

// Solution 2: Bind
let person5 = {
    name: 'Bob',
    friends: ['Alice', 'Charlie'],
    showFriends() {
        this.friends.forEach(function(friend) {
            console.log(`${this.name} knows ${friend}`);
        }.bind(this));
    }
};

🏗️ Constructor Functions and Prototypes

// Constructor function (before ES6 classes)
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// Add method to prototype
Person.prototype.greet = function() {
    return `Hi, I'm ${this.name}`;
};

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

// Create instances
let john = new Person('John', 30);
let jane = new Person('Jane', 25);

console.log(john.greet());  // 'Hi, I'm John'
console.log(jane.greet());  // 'Hi, I'm Jane'

// Prototype chain
console.log(john.hasOwnProperty('name'));   // true
console.log(john.hasOwnProperty('greet'));  // false (on prototype)

// Check prototype
console.log(Object.getPrototypeOf(john) === Person.prototype);  // true
console.log(john instanceof Person);  // true

// Add property to specific instance
john.city = 'New York';
console.log(john.city);  // 'New York'
console.log(jane.city);  // undefined

🎓 ES6 Classes

// Modern class syntax
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        return `Hi, I'm ${this.name}`;
    }
    
    getAge() {
        return this.age;
    }
    
    // Getter
    get info() {
        return `${this.name}, ${this.age}`;
    }
    
    // Setter
    set info(value) {
        [this.name, this.age] = value.split(', ');
    }
    
    // Static method (called on class, not instance)
    static compare(person1, person2) {
        return person1.age - person2.age;
    }
}

let john = new Person('John', 30);
let jane = new Person('Jane', 25);

console.log(john.greet());  // 'Hi, I'm John'
console.log(john.info);     // 'John, 30'

john.info = 'John Doe, 31';
console.log(john.name);     // 'John Doe'

console.log(Person.compare(john, jane));  // 6

// Inheritance
class Student extends Person {
    constructor(name, age, grade) {
        super(name, age);  // Call parent constructor
        this.grade = grade;
    }
    
    study() {
        return `${this.name} is studying`;
    }
    
    // Override parent method
    greet() {
        return `${super.greet()} and I'm a student`;
    }
}

let student = new Student('Bob', 20, 'A');
console.log(student.greet());   // 'Hi, I'm Bob and I'm a student'
console.log(student.study());   // 'Bob is studying'
console.log(student instanceof Student);  // true
console.log(student instanceof Person);   // true

🔄 Object Destructuring

let person = {
    name: 'John',
    age: 30,
    city: 'New York',
    country: 'USA'
};

// Basic destructuring
let { name, age } = person;
console.log(name);  // 'John'
console.log(age);   // 30

// Rename variables
let { name: personName, age: personAge } = person;
console.log(personName);  // 'John'

// Default values
let { name, email = 'N/A' } = person;
console.log(email);  // 'N/A'

// Rest properties
let { name, ...rest } = person;
console.log(rest);  // { age: 30, city: 'New York', country: 'USA' }

// Nested destructuring
let user = {
    name: 'Jane',
    address: {
        city: 'Boston',
        zip: '02101'
    }
};

let { address: { city, zip } } = user;
console.log(city);  // 'Boston'

// Function parameters
function greet({ name, age = 18 }) {
    return `Hello ${name}, you are ${age}`;
}

console.log(greet({ name: 'John', age: 30 }));  // 'Hello John, you are 30'
console.log(greet({ name: 'Jane' }));           // 'Hello Jane, you are 18'

💡 Practical Examples

Deep Clone

// Shallow clone (only copies first level)
let person = {
    name: 'John',
    address: { city: 'NYC' }
};

let shallow = { ...person };
shallow.address.city = 'Boston';
console.log(person.address.city);  // 'Boston' (both changed!)

// Deep clone with JSON (simple but limited)
let deep1 = JSON.parse(JSON.stringify(person));
// Limitations: loses functions, undefined, symbols, dates

// Deep clone with recursion
function deepClone(obj) {
    if (obj === null || typeof obj !== 'object') return obj;
    
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof Array) return obj.map(item => deepClone(item));
    
    let cloned = {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            cloned[key] = deepClone(obj[key]);
        }
    }
    return cloned;
}

// Modern: structuredClone (ES2022)
let deep2 = structuredClone(person);

Object Comparison

// Objects are compared by reference
let obj1 = { name: 'John' };
let obj2 = { name: 'John' };
let obj3 = obj1;

console.log(obj1 === obj2);  // false (different objects)
console.log(obj1 === obj3);  // true (same reference)

// Compare by value
function areEqual(obj1, obj2) {
    let keys1 = Object.keys(obj1);
    let keys2 = Object.keys(obj2);
    
    if (keys1.length !== keys2.length) return false;
    
    for (let key of keys1) {
        if (obj1[key] !== obj2[key]) return false;
    }
    
    return true;
}

console.log(areEqual(obj1, obj2));  // true

// Deep comparison (shallow version above)
function deepEqual(obj1, obj2) {
    if (obj1 === obj2) return true;
    
    if (typeof obj1 !== 'object' || typeof obj2 !== 'object' ||
        obj1 === null || obj2 === null) {
        return false;
    }
    
    let keys1 = Object.keys(obj1);
    let keys2 = Object.keys(obj2);
    
    if (keys1.length !== keys2.length) return false;
    
    for (let key of keys1) {
        if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
            return false;
        }
    }
    
    return true;
}

Object Factory

// Create objects with private data
function createUser(name, email) {
    let isActive = true;  // Private variable
    
    return {
        name,
        email,
        getStatus() {
            return isActive ? 'Active' : 'Inactive';
        },
        activate() {
            isActive = true;
        },
        deactivate() {
            isActive = false;
        }
    };
}

let user = createUser('John', 'john@example.com');
console.log(user.name);         // 'John'
console.log(user.getStatus());  // 'Active'
user.deactivate();
console.log(user.getStatus());  // 'Inactive'
// console.log(user.isActive);  // undefined (private)

Object Validation

function validateUser(user) {
    const required = ['name', 'email', 'age'];
    const errors = [];
    
    // Check required fields
    for (let field of required) {
        if (!(field in user)) {
            errors.push(`${field} is required`);
        }
    }
    
    // Validate email
    if (user.email && !user.email.includes('@')) {
        errors.push('Invalid email format');
    }
    
    // Validate age
    if (user.age && (user.age < 0 || user.age > 150)) {
        errors.push('Age must be between 0 and 150');
    }
    
    return {
        valid: errors.length === 0,
        errors
    };
}

console.log(validateUser({ name: 'John', email: 'john@ex.com', age: 30 }));
// { valid: true, errors: [] }

console.log(validateUser({ name: 'Jane' }));
// { valid: false, errors: ['email is required', 'age is required'] }

Object Transform

// Transform object keys/values
function mapObject(obj, fn) {
    return Object.fromEntries(
        Object.entries(obj).map(([key, value]) => fn(key, value))
    );
}

let prices = { apple: 1.5, banana: 0.75, orange: 2 };

// Double all prices
let doubled = mapObject(prices, (key, value) => [key, value * 2]);
console.log(doubled);  // { apple: 3, banana: 1.5, orange: 4 }

// Uppercase keys
let upper = mapObject(prices, (key, value) => [key.toUpperCase(), value]);
console.log(upper);  // { APPLE: 1.5, BANANA: 0.75, ORANGE: 2 }

// Pick specific properties
function pick(obj, keys) {
    return Object.fromEntries(
        keys.filter(key => key in obj).map(key => [key, obj[key]])
    );
}

let user = { name: 'John', age: 30, email: 'john@ex.com', password: '123' };
let safe = pick(user, ['name', 'email']);
console.log(safe);  // { name: 'John', email: 'john@ex.com' }

// Omit properties
function omit(obj, keys) {
    return Object.fromEntries(
        Object.entries(obj).filter(([key]) => !keys.includes(key))
    );
}

let public = omit(user, ['password']);
console.log(public);  // { name: 'John', age: 30, email: 'john@ex.com' }

🎯 Key Takeaways