Lesson 9: Working with Objects and Object Patterns

What You'll Learn

  • Object destructuring
  • Object methods and shorthand
  • Nested objects
  • Spreading and copying objects
  • Common object patterns
  • Object validation techniques

Why This Matters

Objects are fundamental to JavaScript. Understanding how to structure, manipulate, and validate objects will help you write cleaner, more maintainable code. In real-world applications, you'll constantly work with complex data structures represented as objects.


Part 1: Object Destructuring

Create a new file: lesson-09/object-patterns.js

Basic Destructuring

Instead of accessing properties one by one, destructure them:

const person = {
  name: "Alice",
  age: 30,
  email: "alice@example.com"
};

// Old way
const name = person.name;
const age = person.age;
const email = person.email;

// New way (destructuring)
const { name, age, email } = person;

console.log(name);   // Alice
console.log(age);    // 30
console.log(email);  // alice@example.com

Renaming During Destructuring

const person = {
  name: "Alice",
  age: 30
};

// Rename 'name' to 'personName'
const { name: personName, age: personAge } = person;

console.log(personName);  // Alice
console.log(personAge);   // 30

Default Values

const person = {
  name: "Alice"
  // age is missing
};

// Provide default value
const { name, age = 25 } = person;

console.log(name);  // Alice
console.log(age);   // 25 (default used)

Destructuring Function Parameters

function greetPerson({ name, age }) {
  console.log(`Hello, ${name}! You are ${age} years old.`);
}

const person = { name: "Alice", age: 30 };
greetPerson(person);  // Hello, Alice! You are 30 years old.

Part 2: Object Shorthand and Methods

Property Shorthand

const name = "Alice";
const age = 30;

// Old way
const person = {
  name: name,
  age: age
};

// New way (shorthand)
const person2 = {
  name,  // Same as name: name
  age    // Same as age: age
};

Method Shorthand

// Old way
const person = {
  name: "Alice",
  greet: function() {
    console.log(`Hello, I'm ${this.name}`);
  }
};

// New way (method shorthand)
const person2 = {
  name: "Alice",
  greet() {
    console.log(`Hello, I'm ${this.name}`);
  }
};

person2.greet();  // Hello, I'm Alice

Computed Property Names

const propertyName = "age";
const propertyValue = 30;

const person = {
  name: "Alice",
  [propertyName]: propertyValue  // age: 30
};

console.log(person.age);  // 30

Part 3: Nested Objects

Working with Nested Objects

const user = {
  name: "Alice",
  age: 30,
  address: {
    street: "123 Main St",
    city: "New York",
    country: "USA"
  },
  social: {
    twitter: "@alice",
    github: "alice123"
  }
};

// Accessing nested properties
console.log(user.address.city);        // New York
console.log(user.social.twitter);      // @alice

// Destructuring nested objects
const { address: { city, country }, social: { twitter } } = user;
console.log(city);     // New York
console.log(country);  // USA
console.log(twitter);  // @alice

Optional Chaining

Safely access nested properties that might not exist:

const user = {
  name: "Alice"
  // address is missing
};

// Old way - might cause error
// console.log(user.address.city);  // ERROR!

// Safe way - check each level
console.log(user.address && user.address.city);  // undefined

// Best way - optional chaining
console.log(user.address?.city);  // undefined (no error!)

Part 4: Spreading and Copying Objects

Spread Operator

const person = {
  name: "Alice",
  age: 30
};

// Copy and add properties
const employee = {
  ...person,           // Copy all properties from person
  job: "Developer",
  salary: 80000
};

console.log(employee);
// { name: "Alice", age: 30, job: "Developer", salary: 80000 }

Merging Objects

const defaults = {
  theme: "light",
  fontSize: 14,
  notifications: true
};

const userPreferences = {
  fontSize: 16,
  notifications: false
};

// Merge (later properties override earlier ones)
const settings = {
  ...defaults,
  ...userPreferences
};

console.log(settings);
// { theme: "light", fontSize: 16, notifications: false }

Shallow vs Deep Copy

const original = {
  name: "Alice",
  address: {
    city: "New York"
  }
};

// Shallow copy (only copies top level)
const shallowCopy = { ...original };
shallowCopy.address.city = "Boston";

console.log(original.address.city);  // Boston (changed!)
console.log(shallowCopy.address.city);  // Boston

// Deep copy (copies all levels)
const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.address.city = "Chicago";

console.log(original.address.city);  // Boston (unchanged!)
console.log(deepCopy.address.city);  // Chicago

Part 5: Object Factory Functions

Create objects with consistent structure:

function createPerson(name, age, email) {
  return {
    name,
    age,
    email,
    greet() {
      console.log(`Hello, I'm ${this.name}`);
    },
    getInfo() {
      return ${this.name} (${this.age});
    }
  };
}

const alice = createPerson("Alice", 30, "alice@example.com");
const bob = createPerson("Bob", 25, "bob@example.com");

alice.greet();  // Hello, I'm Alice
console.log(bob.getInfo());  // Bob (25)

Factory with Validation

function createUser(name, age, email) {
  // Validation
  if (!name || typeof name !== "string") {
    throw new Error("Name must be a non-empty string");
  }
  
  if (!age || age < 0 || age > 120) {
    throw new Error("Age must be between 0 and 120");
  }
  
  if (!email || !email.includes("@")) {
    throw new Error("Invalid email address");
  }
  
  // Return valid object
  return {
    name,
    age,
    email,
    createdAt: new Date()
  };
}

try {
  const user = createUser("Alice", 30, "alice@example.com");
  console.log(user);
} catch (error) {
  console.error(error.message);
}

Part 6: Object Validation Patterns

Property Existence Checks

function validatePerson(obj) {
  const requiredProps = ["name", "age", "email"];
  const missing = [];
  
  for (const prop of requiredProps) {
    if (!(prop in obj)) {
      missing.push(prop);
    }
  }
  
  if (missing.length > 0) {
    throw new Error(Missing properties: ${missing.join(&quot;, &quot;)});
  }
  
  return true;
}

const person = { name: "Alice", age: 30 };
// validatePerson(person);  // Error: Missing properties: email

Type Validation

function validateUser(user) {
  if (typeof user.name !== "string") {
    throw new Error("Name must be a string");
  }
  
  if (typeof user.age !== "number" || user.age < 0) {
    throw new Error("Age must be a positive number");
  }
  
  if (typeof user.email !== "string" || !user.email.includes("@")) {
    throw new Error("Invalid email");
  }
  
  return true;
}

Schema Validation Function

function validateSchema(obj, schema) {
  for (const [key, validator] of Object.entries(schema)) {
    if (!(key in obj)) {
      throw new Error(Missing property: ${key});
    }
    
    if (!validator(obj[key])) {
      throw new Error(Invalid value for ${key});
    }
  }
  
  return true;
}

// Define schema
const userSchema = {
  name: (value) => typeof value === "string" && value.length > 0,
  age: (value) => typeof value === "number" && value > 0 && value < 120,
  email: (value) => typeof value === "string" && value.includes("@")
};

// Validate
const user = { name: "Alice", age: 30, email: "alice@example.com" };
validateSchema(user, userSchema);  // Pass!

const invalidUser = { name: "", age: -5, email: "invalid" };
// validateSchema(invalidUser, userSchema);  // Error!

Part 7: Common Object Patterns

Options Object Pattern

function createWindow(options = {}) {
  // Provide defaults
  const defaults = {
    width: 800,
    height: 600,
    title: "New Window",
    resizable: true
  };
  
  // Merge with user options
  const config = { ...defaults, ...options };
  
  console.log(`Creating window: ${config.title}`);
  console.log(`Size: ${config.width}x${config.height}`);
  console.log(`Resizable: ${config.resizable}`);
}

createWindow();  // Uses all defaults
createWindow({ title: "My App", width: 1024 });  // Override some

Builder Pattern

class QueryBuilder {
  constructor() {
    this.query = {};
  }
  
  select(fields) {
    this.query.select = fields;
    return this;  // Return this for chaining
  }
  
  from(table) {
    this.query.from = table;
    return this;
  }
  
  where(condition) {
    this.query.where = condition;
    return this;
  }
  
  build() {
    return this.query;
  }
}

const query = new QueryBuilder()
  .select(["name", "age"])
  .from("users")
  .where("age > 18")
  .build();

console.log(query);
// { select: ["name", "age"], from: "users", where: "age > 18" }

Practice Exercises

Exercise 1: User Profile

Create a factory function that creates user profiles with validation.

Exercise 2: Settings Manager

Build a settings object with defaults, merging, and validation.

Exercise 3: Data Transformer

Write a function that transforms nested objects from one structure to another.

Exercise 4: Object Validator

Create a reusable validator that can validate any object against a schema.


Key Concepts Summary

Concept Purpose Example
Destructuring Extract properties easily const { name, age } = person
Spread Copy/merge objects { ...obj1, ...obj2 }
Optional chaining Safe nested access obj?.prop?.nested
Factory function Create objects consistently createUser(name, age)
Validation Ensure data integrity validateSchema(obj, schema)

What You Learned

  • ✅ How to destructure objects efficiently
  • ✅ Object shorthand and method syntax
  • ✅ Working with nested objects safely
  • ✅ Copying and merging objects
  • ✅ Creating factory functions with validation
  • ✅ Common object patterns for real applications

What's Next?

In the next lesson, you'll learn about array methods - powerful tools for transforming, filtering, and processing arrays of data!