JavaScript Essentials

Objects: Creating & Accessing Properties

45 min Lesson 16 of 60

What Are Objects in JavaScript?

Objects are one of the most fundamental data types in JavaScript. While arrays store ordered collections of values accessed by index, objects store key-value pairs where each value is associated with a named key (also called a property). Objects are the building blocks of nearly every JavaScript application -- they represent users, products, settings, API responses, DOM elements, and virtually any entity you need to model in your code. Understanding how to create, access, modify, and work with objects is essential for every JavaScript developer.

In JavaScript, almost everything is an object or behaves like one. Arrays are objects, functions are objects, dates are objects, and even primitive values like strings and numbers have object wrappers that give them access to methods. When people say JavaScript is an "object-oriented" language, this pervasive use of objects is what they mean. In this lesson, we will focus on plain objects (also called object literals) -- the most common and versatile object type you will use in everyday development.

Objects differ from arrays in several important ways. Arrays are ordered and accessed by numeric index. Objects are unordered collections accessed by string keys. Arrays are ideal for lists of similar items (a list of products, a list of scores). Objects are ideal for representing a single entity with multiple named properties (a single user with name, email, and age). Choosing between an array and an object depends on what your data represents and how you need to access it.

Creating Objects with Object Literals

The simplest and most common way to create an object in JavaScript is with an object literal -- a pair of curly braces containing zero or more key-value pairs separated by commas. Each key is followed by a colon and its corresponding value. Keys are typically strings (though they can be symbols), and values can be any valid JavaScript data type including strings, numbers, booleans, arrays, functions, and even other objects.

Example: Creating Objects with Literals

// An empty object
const emptyObj = {};

// An object with several properties
const person = {
    firstName: 'Ahmad',
    lastName: 'Hassan',
    age: 28,
    isStudent: false,
    hobbies: ['reading', 'coding', 'hiking'],
    address: {
        city: 'Amman',
        country: 'Jordan'
    }
};

// Object with different value types
const config = {
    apiUrl: 'https://api.example.com',
    timeout: 5000,
    retryCount: 3,
    debug: true,
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token123'
    }
};

Property Key Rules

Property keys in JavaScript objects follow specific rules. If the key is a valid identifier (starts with a letter, underscore, or dollar sign and contains only letters, digits, underscores, or dollar signs), you can write it without quotes. If the key contains spaces, hyphens, starts with a number, or is a reserved word, you must wrap it in quotes.

Example: Valid and Quoted Property Keys

const product = {
    name: 'Laptop',              // valid identifier -- no quotes needed
    price: 999,                   // valid identifier
    _id: 'abc123',               // valid (starts with underscore)
    $currency: 'USD',            // valid (starts with dollar sign)
    'in-stock': true,            // hyphen requires quotes
    'item count': 42,            // space requires quotes
    '2ndChoice': 'Monitor',     // starts with number requires quotes
    'class': 'premium'           // reserved word -- quotes recommended
};
Note: Even though JavaScript allows unquoted property keys for valid identifiers, all property keys are internally stored as strings. The key name is the same as 'name'. Numeric keys are also converted to strings -- { 1: 'one' } stores the key as the string '1'.

Accessing Properties: Dot Notation vs Bracket Notation

There are two ways to access properties on an object: dot notation and bracket notation. Understanding when to use each one is crucial for working effectively with objects.

Dot Notation

Dot notation is the most common and readable way to access object properties. You write the object name, followed by a dot, followed by the property name. Dot notation only works when the property name is a valid JavaScript identifier.

Example: Dot Notation

const user = {
    name: 'Sara Ali',
    email: 'sara@example.com',
    age: 25,
    isActive: true
};

// Reading properties
console.log(user.name);      // 'Sara Ali'
console.log(user.email);     // 'sara@example.com'
console.log(user.age);       // 25
console.log(user.isActive);  // true

// Accessing nested objects
const company = {
    name: 'TechCorp',
    address: {
        street: '123 Main St',
        city: 'Dubai',
        zip: '12345'
    }
};

console.log(company.address.city);    // 'Dubai'
console.log(company.address.street);  // '123 Main St'

Bracket Notation

Bracket notation uses square brackets with a string (or expression that evaluates to a string) to access properties. Bracket notation is more flexible than dot notation because it works with any string key, including those with spaces, hyphens, or dynamic values stored in variables.

Example: Bracket Notation

const user = {
    name: 'Omar Khan',
    'favorite-color': 'blue',
    'home address': '123 Palm Ave',
    age: 30
};

// Bracket notation with string keys
console.log(user['name']);           // 'Omar Khan'
console.log(user['favorite-color']); // 'blue'    -- cannot use dot notation here
console.log(user['home address']);   // '123 Palm Ave' -- cannot use dot notation here

// Dynamic property access with variables
const propertyName = 'age';
console.log(user[propertyName]);     // 30

// Using expressions
const prefix = 'favorite';
console.log(user[prefix + '-color']); // 'blue'

// Iterating and accessing dynamically
const keys = ['name', 'age'];
keys.forEach(function(key) {
    console.log(key + ': ' + user[key]);
});
// Output: name: Omar Khan, age: 30
Pro Tip: Use dot notation whenever possible because it is cleaner and easier to read. Switch to bracket notation only when you need to: access properties with special characters in their names, use a variable or expression as the property key, or work with dynamically generated property names.

Computed Property Names

ES6 introduced computed property names, which let you use an expression inside square brackets as a property key when creating an object literal. This is extremely useful when you need to create objects with dynamic keys -- for example, when building objects from user input, API responses, or configuration data.

Example: Computed Property Names

// Basic computed property
const key = 'color';
const value = 'red';

const obj = {
    [key]: value
};
console.log(obj); // { color: 'red' }

// Using expressions in computed properties
const prefix = 'user';
const userSettings = {
    [prefix + 'Name']: 'Ahmad',
    [prefix + 'Email']: 'ahmad@example.com',
    [prefix + 'Role']: 'admin'
};
console.log(userSettings);
// { userName: 'Ahmad', userEmail: 'ahmad@example.com', userRole: 'admin' }

// Dynamic form data processing
function createFormData(fieldName, fieldValue) {
    return {
        [fieldName]: fieldValue,
        [fieldName + '_updated']: new Date().toISOString()
    };
}

const data = createFormData('email', 'test@example.com');
console.log(data);
// { email: 'test@example.com', email_updated: '2024-01-15T10:30:00.000Z' }

// Building lookup objects dynamically
const categories = ['electronics', 'clothing', 'books'];
const categoryFlags = categories.reduce(function(flags, cat) {
    return { ...flags, ['show_' + cat]: true };
}, {});
console.log(categoryFlags);
// { show_electronics: true, show_clothing: true, show_books: true }

Shorthand Property and Method Syntax

ES6 introduced two shorthand syntaxes that make creating objects more concise. When a property value comes from a variable with the same name as the key, you can use property shorthand. When defining methods (functions as property values), you can use method shorthand that omits the function keyword and colon.

Example: Property and Method Shorthand

// Property shorthand
const name = 'Layla';
const age = 27;
const city = 'Riyadh';

// Without shorthand (old way)
const personOld = {
    name: name,
    age: age,
    city: city
};

// With shorthand (modern way)
const personNew = { name, age, city };
console.log(personNew); // { name: 'Layla', age: 27, city: 'Riyadh' }

// Method shorthand
// Without shorthand
const calculatorOld = {
    add: function(a, b) {
        return a + b;
    },
    subtract: function(a, b) {
        return a - b;
    }
};

// With shorthand
const calculator = {
    add(a, b) {
        return a + b;
    },
    subtract(a, b) {
        return a - b;
    }
};

console.log(calculator.add(10, 5));      // 15
console.log(calculator.subtract(10, 5)); // 5

// Combining both shorthands
function createUser(name, email, role) {
    return {
        name,
        email,
        role,
        createdAt: new Date().toISOString(),
        greet() {
            return 'Hello, I am ' + this.name;
        }
    };
}

const user = createUser('Ahmad', 'ahmad@example.com', 'admin');
console.log(user.greet()); // 'Hello, I am Ahmad'

Adding, Modifying, and Deleting Properties

JavaScript objects are mutable -- you can add new properties, change existing ones, and delete properties at any time after the object is created. This is true even for objects declared with const, because const prevents reassignment of the variable, not modification of the object it points to.

Example: Adding and Modifying Properties

const car = {
    make: 'Toyota',
    model: 'Camry',
    year: 2023
};

// Adding new properties
car.color = 'silver';
car['mileage'] = 15000;
car.features = ['bluetooth', 'backup camera'];

console.log(car);
// { make: 'Toyota', model: 'Camry', year: 2023,
//   color: 'silver', mileage: 15000, features: [...] }

// Modifying existing properties
car.year = 2024;
car.mileage = 16500;
console.log(car.year);    // 2024
console.log(car.mileage); // 16500

// Deleting properties
delete car.mileage;
console.log(car.mileage); // undefined
console.log(car);
// { make: 'Toyota', model: 'Camry', year: 2024,
//   color: 'silver', features: [...] }

// const prevents reassignment, not mutation
const settings = { theme: 'dark' };
settings.theme = 'light';    // This works -- mutating the object
settings.fontSize = 16;      // This works -- adding a property

// settings = { theme: 'new' }; // TypeError: Assignment to constant variable
Common Mistake: Thinking that const makes an object immutable. The const keyword only prevents the variable from being reassigned to a different value. The object itself can still be modified. If you need a truly immutable object, use Object.freeze(), but be aware that it only performs a shallow freeze -- nested objects can still be modified.

Checking Property Existence

Before accessing a property, you often need to check whether it exists on an object. JavaScript provides several ways to do this, each with slightly different behavior.

The in Operator

The in operator checks if a property exists in an object, including properties inherited from the prototype chain. It returns true or false.

The hasOwnProperty Method

The hasOwnProperty method checks if the object has the specified property as its own property (not inherited from the prototype chain). This is the safer option when you want to check only for properties that belong directly to the object.

Example: Checking Property Existence

const user = {
    name: 'Sara',
    email: 'sara@example.com',
    age: 25,
    address: undefined
};

// Using the in operator
console.log('name' in user);     // true
console.log('email' in user);    // true
console.log('phone' in user);    // false
console.log('address' in user);  // true (exists, even though value is undefined)
console.log('toString' in user); // true (inherited from Object prototype)

// Using hasOwnProperty
console.log(user.hasOwnProperty('name'));     // true
console.log(user.hasOwnProperty('phone'));    // false
console.log(user.hasOwnProperty('address'));  // true
console.log(user.hasOwnProperty('toString')); // false (inherited, not own)

// Checking before accessing -- avoiding errors
function getUserCity(user) {
    if (user.address && user.address.city) {
        return user.address.city;
    }
    return 'City not available';
}

// Modern approach: optional chaining (ES2020)
function getUserCityModern(user) {
    return user?.address?.city ?? 'City not available';
}

// Practical pattern: safe property access
const config = { database: { host: 'localhost', port: 3306 } };
const dbHost = ('database' in config) ? config.database.host : 'default-host';
console.log(dbHost); // 'localhost'
Note: There is an important difference between a property that does not exist and a property that exists with the value undefined. The in operator and hasOwnProperty both return true for properties set to undefined, while simply checking the value (if (obj.prop)) would return false. Use the right check for your specific situation.

Object.keys, Object.values, and Object.entries

JavaScript provides three static methods on the Object constructor that let you extract the keys, values, or key-value pairs from an object as arrays. These methods are incredibly useful for iterating over objects, transforming their data, and converting between objects and arrays.

Example: Object.keys, Object.values, Object.entries

const product = {
    name: 'Mechanical Keyboard',
    price: 89.99,
    brand: 'KeyMaster',
    inStock: true,
    rating: 4.7
};

// Object.keys -- returns array of property names
const keys = Object.keys(product);
console.log(keys);
// ['name', 'price', 'brand', 'inStock', 'rating']

// Object.values -- returns array of property values
const values = Object.values(product);
console.log(values);
// ['Mechanical Keyboard', 89.99, 'KeyMaster', true, 4.7]

// Object.entries -- returns array of [key, value] pairs
const entries = Object.entries(product);
console.log(entries);
// [
//   ['name', 'Mechanical Keyboard'],
//   ['price', 89.99],
//   ['brand', 'KeyMaster'],
//   ['inStock', true],
//   ['rating', 4.7]
// ]

// Iterating over an object with forEach
Object.keys(product).forEach(function(key) {
    console.log(key + ': ' + product[key]);
});

// Using entries with destructuring
Object.entries(product).forEach(function([key, value]) {
    console.log(key + ' = ' + value);
});

// Converting an object to an array of formatted strings
const summary = Object.entries(product)
    .map(function([key, value]) {
        return key + ': ' + value;
    })
    .join(', ');
console.log(summary);
// 'name: Mechanical Keyboard, price: 89.99, brand: KeyMaster, ...'

// Counting properties
console.log('Property count: ' + Object.keys(product).length); // 5

Object.assign and Merging Objects

The Object.assign method copies all enumerable own properties from one or more source objects to a target object. It returns the modified target object. This method is commonly used for merging objects, creating copies, and applying default settings.

Example: Object.assign

// Merging objects
const defaults = {
    theme: 'light',
    fontSize: 14,
    language: 'en',
    notifications: true
};

const userPrefs = {
    theme: 'dark',
    fontSize: 18
};

// Merge user preferences with defaults
const settings = Object.assign({}, defaults, userPrefs);
console.log(settings);
// { theme: 'dark', fontSize: 18, language: 'en', notifications: true }
// User preferences override defaults

// Creating a shallow copy
const original = { a: 1, b: 2, c: { nested: true } };
const copy = Object.assign({}, original);

copy.a = 99;
console.log(original.a); // 1 -- primitive values are independent

copy.c.nested = false;
console.log(original.c.nested); // false -- nested objects share the same reference!

// Multiple source objects (later sources override earlier ones)
const base = { a: 1, b: 2 };
const override1 = { b: 3, c: 4 };
const override2 = { c: 5, d: 6 };

const merged = Object.assign({}, base, override1, override2);
console.log(merged); // { a: 1, b: 3, c: 5, d: 6 }

Object Spread Syntax

The spread operator (...) provides a more modern and readable alternative to Object.assign for copying and merging objects. Introduced in ES2018, object spread creates a new object by spreading the properties of existing objects into it. Like Object.assign, it performs a shallow copy.

Example: Object Spread

// Copying an object
const original = { name: 'Sara', age: 25 };
const copy = { ...original };
console.log(copy); // { name: 'Sara', age: 25 }

// Merging objects
const defaults = { theme: 'light', lang: 'en', debug: false };
const userPrefs = { theme: 'dark', debug: true };

const config = { ...defaults, ...userPrefs };
console.log(config);
// { theme: 'dark', lang: 'en', debug: true }

// Adding new properties while copying
const baseProduct = { name: 'Widget', price: 10 };
const featuredProduct = {
    ...baseProduct,
    featured: true,
    discount: 0.1
};
console.log(featuredProduct);
// { name: 'Widget', price: 10, featured: true, discount: 0.1 }

// Overriding specific properties
const user = { name: 'Ahmad', email: 'ahmad@old.com', role: 'user' };
const updatedUser = { ...user, email: 'ahmad@new.com', role: 'admin' };
console.log(updatedUser);
// { name: 'Ahmad', email: 'ahmad@new.com', role: 'admin' }

// Conditional properties using spread
const isAdmin = true;
const userObj = {
    name: 'Omar',
    email: 'omar@example.com',
    ...(isAdmin ? { role: 'admin', permissions: ['read', 'write', 'delete'] } : { role: 'user' })
};
console.log(userObj);
// { name: 'Omar', email: 'omar@example.com', role: 'admin', permissions: [...] }
Pro Tip: Prefer the spread syntax over Object.assign in modern JavaScript. It is more readable, creates a new object by default (no need for the empty object {} as the first argument), and integrates cleanly with other spread operations on arrays. Both perform shallow copies, so nested objects still share references.

Working with Nested Objects

Real-world data is rarely flat. Objects frequently contain other objects, arrays of objects, and deeply nested structures. Working with nested objects requires understanding how to safely access deep properties, how to modify nested values without mutating the original, and how to handle missing or undefined intermediate properties.

Example: Nested Objects

const company = {
    name: 'TechStartup',
    founded: 2020,
    headquarters: {
        address: {
            street: '456 Innovation Blvd',
            city: 'Dubai',
            country: 'UAE'
        },
        contact: {
            phone: '+971-4-123-4567',
            email: 'info@techstartup.com'
        }
    },
    departments: {
        engineering: {
            head: 'Ahmad',
            teamSize: 15,
            projects: ['website', 'mobile-app', 'api']
        },
        marketing: {
            head: 'Sara',
            teamSize: 8,
            projects: ['rebrand', 'social-media']
        }
    }
};

// Accessing deeply nested values
console.log(company.headquarters.address.city); // 'Dubai'
console.log(company.departments.engineering.head); // 'Ahmad'
console.log(company.departments.engineering.projects[0]); // 'website'

// Safe access with optional chaining
const salesHead = company.departments?.sales?.head ?? 'Not assigned';
console.log(salesHead); // 'Not assigned'

// Modifying nested values (mutates the original)
company.departments.engineering.teamSize = 18;
company.departments.engineering.projects.push('dashboard');

// Creating a modified copy without mutating the original
const updatedCompany = {
    ...company,
    headquarters: {
        ...company.headquarters,
        contact: {
            ...company.headquarters.contact,
            phone: '+971-4-999-8888'
        }
    }
};

console.log(company.headquarters.contact.phone);        // '+971-4-123-4567' (unchanged)
console.log(updatedCompany.headquarters.contact.phone);  // '+971-4-999-8888' (updated copy)
Common Mistake: Trying to access properties on undefined or null values in a nested chain will throw a TypeError. For example, company.departments.sales.head will crash if sales does not exist. Always use optional chaining (?.) or check each level before accessing the next when working with nested data that might have missing properties.

Object Comparison: Reference vs Value

One of the most important and frequently misunderstood concepts in JavaScript is how objects are compared. Unlike primitive values (numbers, strings, booleans) which are compared by value, objects are compared by reference. Two objects with identical properties and values are NOT equal unless they point to the exact same object in memory.

Example: Reference vs Value Comparison

// Primitive comparison: by VALUE
const a = 5;
const b = 5;
console.log(a === b); // true -- same value

const str1 = 'hello';
const str2 = 'hello';
console.log(str1 === str2); // true -- same value

// Object comparison: by REFERENCE
const obj1 = { name: 'Ahmad', age: 30 };
const obj2 = { name: 'Ahmad', age: 30 };
console.log(obj1 === obj2); // false! Different objects in memory

// Same reference
const obj3 = obj1;
console.log(obj1 === obj3); // true -- both point to the same object

// Modifying through one reference affects the other
obj3.age = 31;
console.log(obj1.age); // 31 -- obj1 is affected because they share a reference

// Arrays are objects too -- same reference behavior
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
console.log(arr1 === arr2); // false

// How to compare objects by value
// Method 1: JSON.stringify (works for simple objects)
const user1 = { name: 'Sara', age: 25 };
const user2 = { name: 'Sara', age: 25 };
console.log(JSON.stringify(user1) === JSON.stringify(user2)); // true

// Method 2: Manual comparison function
function shallowEqual(objA, objB) {
    const keysA = Object.keys(objA);
    const keysB = Object.keys(objB);

    if (keysA.length !== keysB.length) return false;

    return keysA.every(function(key) {
        return objB.hasOwnProperty(key) && objA[key] === objB[key];
    });
}

console.log(shallowEqual(user1, user2)); // true
Note: The JSON.stringify approach for comparing objects has limitations. It does not work with undefined values (they are omitted), functions (they are omitted), Date objects (converted to strings), and circular references (throws an error). It also requires properties to be in the same order. For production code, consider using a deep equality function from a library like Lodash.

Iterating Over Objects

Unlike arrays, objects do not have built-in iteration methods like forEach or map. However, there are several effective patterns for iterating over object properties.

Example: Different Ways to Iterate Over Objects

const scores = {
    math: 92,
    science: 88,
    english: 95,
    history: 79,
    art: 97
};

// Method 1: for...in loop
for (const subject in scores) {
    if (scores.hasOwnProperty(subject)) {
        console.log(subject + ': ' + scores[subject]);
    }
}

// Method 2: Object.keys with forEach
Object.keys(scores).forEach(function(subject) {
    console.log(subject + ': ' + scores[subject]);
});

// Method 3: Object.entries with forEach (most modern)
Object.entries(scores).forEach(function([subject, score]) {
    console.log(subject + ': ' + score);
});

// Method 4: Object.entries with for...of
for (const [subject, score] of Object.entries(scores)) {
    console.log(subject + ': ' + score);
}

// Transforming objects using Object.entries and Object.fromEntries
const curved = Object.fromEntries(
    Object.entries(scores).map(function([subject, score]) {
        return [subject, Math.min(score + 5, 100)];
    })
);
console.log(curved);
// { math: 97, science: 93, english: 100, history: 84, art: 100 }

Real-World Object Patterns

Let us look at comprehensive examples that demonstrate how objects are used in real-world JavaScript applications, from managing application state to processing API data and building dynamic configurations.

Example: User Profile Management

// Creating a user profile with methods
const userProfile = {
    id: 1,
    firstName: 'Ahmad',
    lastName: 'Hassan',
    email: 'ahmad@example.com',
    preferences: {
        theme: 'dark',
        language: 'ar',
        notifications: {
            email: true,
            push: false,
            sms: true
        }
    },

    getFullName() {
        return this.firstName + ' ' + this.lastName;
    },

    updatePreference(path, value) {
        const keys = path.split('.');
        let current = this.preferences;
        for (let i = 0; i < keys.length - 1; i++) {
            if (!current[keys[i]]) current[keys[i]] = {};
            current = current[keys[i]];
        }
        current[keys[keys.length - 1]] = value;
    },

    toJSON() {
        return {
            id: this.id,
            name: this.getFullName(),
            email: this.email,
            preferences: { ...this.preferences }
        };
    }
};

console.log(userProfile.getFullName()); // 'Ahmad Hassan'

userProfile.updatePreference('notifications.push', true);
console.log(userProfile.preferences.notifications.push); // true

console.log(userProfile.toJSON());

Example: Dynamic Configuration Builder

// Building a configuration object dynamically
function createApiConfig(options) {
    const defaults = {
        baseUrl: 'https://api.example.com',
        version: 'v1',
        timeout: 5000,
        retries: 3,
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        }
    };

    // Merge with user options
    const config = {
        ...defaults,
        ...options,
        headers: {
            ...defaults.headers,
            ...(options.headers || {})
        }
    };

    // Add computed properties
    config.fullUrl = config.baseUrl + '/' + config.version;

    // Add methods
    config.getEndpoint = function(path) {
        return this.fullUrl + '/' + path;
    };

    return config;
}

const apiConfig = createApiConfig({
    baseUrl: 'https://myapi.com',
    headers: { 'Authorization': 'Bearer token123' }
});

console.log(apiConfig.fullUrl);
// 'https://myapi.com/v1'

console.log(apiConfig.getEndpoint('users'));
// 'https://myapi.com/v1/users'

console.log(apiConfig.headers);
// { 'Content-Type': 'application/json', 'Accept': 'application/json',
//   'Authorization': 'Bearer token123' }

Example: Processing and Restructuring API Data

// Simulated API response with nested data
const apiResponse = {
    status: 200,
    data: {
        users: [
            { id: 1, name: 'Ahmad', department_id: 101, skills: ['js', 'python'] },
            { id: 2, name: 'Sara', department_id: 102, skills: ['design', 'css'] },
            { id: 3, name: 'Omar', department_id: 101, skills: ['js', 'node'] },
            { id: 4, name: 'Layla', department_id: 103, skills: ['marketing'] }
        ],
        departments: [
            { id: 101, name: 'Engineering' },
            { id: 102, name: 'Design' },
            { id: 103, name: 'Marketing' }
        ]
    }
};

// Create a department lookup object for O(1) access
const deptLookup = apiResponse.data.departments.reduce(function(lookup, dept) {
    lookup[dept.id] = dept.name;
    return lookup;
}, {});
console.log(deptLookup); // { 101: 'Engineering', 102: 'Design', 103: 'Marketing' }

// Enrich user data with department names
const enrichedUsers = apiResponse.data.users.map(function(user) {
    return {
        ...user,
        departmentName: deptLookup[user.department_id] || 'Unknown',
        skillCount: user.skills.length
    };
});
console.log(enrichedUsers);

// Group users by department
const usersByDept = enrichedUsers.reduce(function(groups, user) {
    const dept = user.departmentName;
    if (!groups[dept]) {
        groups[dept] = [];
    }
    groups[dept].push({ name: user.name, skills: user.skills });
    return groups;
}, {});

console.log(usersByDept);
// {
//   Engineering: [{ name: 'Ahmad', skills: [...] }, { name: 'Omar', skills: [...] }],
//   Design: [{ name: 'Sara', skills: [...] }],
//   Marketing: [{ name: 'Layla', skills: [...] }]
// }

// Collect all unique skills across all users
const allSkills = apiResponse.data.users
    .reduce(function(skills, user) {
        user.skills.forEach(function(skill) {
            if (!skills.includes(skill)) {
                skills.push(skill);
            }
        });
        return skills;
    }, []);
console.log(allSkills); // ['js', 'python', 'design', 'css', 'node', 'marketing']

Practice Exercise

Build a small contact management system using objects. Complete the following tasks:

  1. Create a contacts object that stores at least 5 contact objects, each with properties for name, email, phone, category (one of 'family', 'work', 'friend'), and an address object with city and country.
  2. Write a function addContact that takes the contacts object, a name, and a contact details object, then adds the new contact using computed property names.
  3. Write a function findByCity that uses Object.values and filter to return all contacts in a specific city.
  4. Write a function getContactSummary that uses Object.entries and map to return an array of formatted strings like "Name (category): email".
  5. Write a function groupByCategory that uses Object.values and reduce to group contacts by their category.
  6. Write a function mergeContacts that takes two contact objects and merges them using the spread operator, with the second object overriding duplicate keys.
  7. Use Object.keys to count how many contacts are in each category.
  8. Create a deep copy of one contact, modify the copy's address, and verify the original is unchanged.