Labs ICT
Pro Login

Objects

JavaScript objects are fundamental data structures that store key-value pairs and provide the foundation for object-oriented programming. Objects enable you to organize related data and functionality into cohesive units, making code more modular and maintainable.

In this tutorial, we'll explore object creation, manipulation, methods, prototypes, classes, and modern object-oriented programming in JavaScript.

Creating Objects

Multiple ways to create objects in JavaScript, each suited for different scenarios.

Object Literal

// Object literal syntax
const person = {
  firstName: 'John',
  lastName: 'Doe',
  age: 30,
  isAdmin: true
};

// Empty object
const emptyObject = {};

// Object with methods
const calculator = {
  add: function(a, b) {
    return a + b;
  },
  subtract: (a, b) => a - b,
  multiply(a, b) {
    return a * b;
  }
};

// Computed property names (ES6)
const propertyName = 'dynamic';
const obj = {
  [propertyName]: 'value',
  ['computed' + 'Property']: 'computed value'
};

// Shorthand property names (ES6)
const name = 'John';
const age = 30;
const person2 = { name, age }; // Same as { name: name, age: age }

// Shorthand method names (ES6)
const methods = {
  greet() { // Same as greet: function()
    console.log('Hello!');
  }
};

Object Constructor

// Object constructor
const person = new Object();
person.firstName = 'John';
person.lastName = 'Doe';
person.age = 30;

// Constructor function
function Person(firstName, lastName, age) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.age = age;
  this.fullName = function() {
    return `${this.firstName} ${this.lastName}`;
  };
}

const john = new Person('John', 'Doe', 30);
console.log(john.fullName()); // "John Doe"

// Using Object.create()
const personProto = {
  greet: function() {
    return `Hello, I'm ${this.firstName}`;
  }
};

const jane = Object.create(personProto);
jane.firstName = 'Jane';
jane.lastName = 'Smith';
console.log(jane.greet()); // "Hello, I'm Jane"

ES6 Classes

// Class declaration
class Person {
  constructor(firstName, lastName, age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
  
  // Method
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
  
  // Getter
  get displayName() {
    return this.fullName();
  }
  
  // Setter
  set displayName(name) {
    const [first, last] = name.split(' ');
    this.firstName = first;
    this.lastName = last;
  }
  
  // Static method
  static createAdult(firstName, lastName) {
    return new Person(firstName, lastName, 18);
  }
}

// Class inheritance
class Employee extends Person {
  constructor(firstName, lastName, age, position, salary) {
    super(firstName, lastName, age); // Call parent constructor
    this.position = position;
    this.salary = salary;
  }
  
  // Override method
  fullName() {
    return `${super.fullName()} (${this.position})`;
  }
  
  // New method
  promote(newPosition, raise = 0.1) {
    this.position = newPosition;
    this.salary *= (1 + raise);
  }
}

const employee = new Employee('John', 'Doe', 30, 'Developer', 50000);
console.log(employee.fullName()); // "John Doe (Developer)"

Object Properties

Working with object properties, including access, modification, and enumeration.

Property Access

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

console.log(person.firstName); // "John"
person.age = 30; // Add property

// Bracket notation
console.log(person['firstName']); // "John"
person['last name'] = 'Smith'; // Property with space

// Dynamic property access
const property = 'firstName';
console.log(person[property]); // "John"

// Computed property access
const computed = 'computed' + 'Property';
person[computed] = 'value';

// Property existence check
console.log('firstName' in person); // true
console.log('nonExistent' in person); // false

// hasOwnProperty
console.log(person.hasOwnProperty('firstName')); // true
console.log(person.hasOwnProperty('toString')); // false (inherited)

Property Descriptors

// Property descriptors
const obj = {};
Object.defineProperty(obj, 'name', {
  value: 'John',
  writable: true,    // Can be changed
  enumerable: true,  // Shows up in for...in
  configurable: true // Can be deleted/configured
});

// Read-only property
Object.defineProperty(obj, 'id', {
  value: 123,
  writable: false
});

// Getters and setters
const person = {
  _firstName: 'John',
  
  get firstName() {
    return this._firstName;
  },
  
  set firstName(value) {
    if (typeof value === 'string' && value.length > 0) {
      this._firstName = value;
    }
  }
};

// Multiple properties
Object.defineProperties(obj, {
  prop1: { value: 'value1', writable: true },
  prop2: { value: 'value2', enumerable: false }
});

// Get property descriptor
const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);

Property Enumeration

// Object.keys() - own enumerable properties
const person = {
  firstName: 'John',
  lastName: 'Doe',
  age: 30
};

console.log(Object.keys(person)); // ['firstName', 'lastName', 'age']

// Object.values() - own enumerable values
console.log(Object.values(person)); // ['John', 'Doe', 30]

// Object.entries() - own enumerable key-value pairs
console.log(Object.entries(person));
// [['firstName', 'John'], ['lastName', 'Doe'], ['age', 30]]

// for...in loop - all enumerable properties (including inherited)
for (const key in person) {
  console.log(key, person[key]);
}

// Safe for...in with hasOwnProperty
for (const key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key, person[key]);
  }
}

// Object.getOwnPropertyNames() - all own properties (enumerable or not)
console.log(Object.getOwnPropertyNames(person));

// Object.getOwnPropertySymbols() - symbol properties
const symbolKey = Symbol('description');
person[symbolKey] = 'symbol value';
console.log(Object.getOwnPropertySymbols(person));

Object Methods

Built-in methods for object manipulation and utility operations.

Object Manipulation

// Object.assign() - copy properties
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const result = Object.assign(target, source);
console.log(result); // { a: 1, b: 3, c: 4 }

// Object spread (ES6) - better alternative
const merged = { ...target, ...source };
console.log(merged); // { a: 1, b: 3, c: 4 }

// Object.create() - create with prototype
const proto = { greet: () => 'Hello' };
const obj = Object.create(proto);
obj.name = 'John';
console.log(obj.greet()); // "Hello"

// Object.freeze() - make immutable
const frozen = Object.freeze({ a: 1, b: 2 });
frozen.a = 3; // Ignored in strict mode
delete frozen.a; // Ignored

// Object.seal() - prevent new properties
const sealed = Object.seal({ a: 1 });
sealed.b = 2; // Ignored
sealed.a = 3; // Allowed

// Object.preventExtensions() - prevent adding properties
const noExt = Object.preventExtensions({ a: 1 });
noExt.b = 2; // Ignored

Object Comparison

// Object.is() - strict equality
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(+0, -0)); // false

// Shallow equality
function shallowEqual(obj1, obj2) {
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  
  if (keys1.length !== keys2.length) return false;
  
  for (const key of keys1) {
    if (obj1[key] !== obj2[key]) return false;
  }
  
  return true;
}

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

Object Utilities

// Object.fromEntries() - create object from entries
const entries = [['name', 'John'], ['age', 30]];
const obj = Object.fromEntries(entries);
console.log(obj); // { name: 'John', age: 30 }

// Pick properties
function pick(obj, keys) {
  const result = {};
  for (const key of keys) {
    if (key in obj) {
      result[key] = obj[key];
    }
  }
  return result;
}

// Omit properties
function omit(obj, keys) {
  const result = { ...obj };
  for (const key of keys) {
    delete result[key];
  }
  return result;
}

// Clone object
function clone(obj) {
  return { ...obj }; // Shallow clone
}

// Deep clone
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(deepClone);
  
  const cloned = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloned[key] = deepClone(obj[key]);
    }
  }
  return cloned;
}

Prototypes and Inheritance

Understanding JavaScript's prototype-based inheritance system.

Prototype Chain

// Prototype chain
function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  return `Hello, I'm ${this.name}`;
};

const john = new Person('John');
console.log(john.greet()); // Method from prototype

// Check prototype
console.log(john.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

// Property lookup
john.hasOwnProperty('name'); // true
john.hasOwnProperty('greet'); // false (from prototype)
'greet' in john; // true (found in prototype chain

Prototype Inheritance

// Constructor inheritance
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  return `${this.name} makes a sound`;
};

function Dog(name, breed) {
  Animal.call(this, name); // Call parent constructor
  this.breed = breed;
}

// Set up prototype chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Override method
Dog.prototype.speak = function() {
  return `${this.name} barks`;
};

// Add method
Dog.prototype.fetch = function() {
  return `${this.name} fetches the ball`;
};

const dog = new Dog('Buddy', 'Golden Retriever');
console.log(dog.speak()); // "Buddy barks"
console.log(dog.fetch()); // "Buddy fetches the ball"

ES6 Class Inheritance

// Class inheritance (syntactic sugar)
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    return `${this.name} makes a sound`;
  }
  
  static kingdom() {
    return 'Animalia';
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call parent constructor
    this.breed = breed;
  }
  
  speak() {
    return `${this.name} barks`;
  }
  
  fetch() {
    return `${this.name} fetches the ball`;
  }
  
  // Override static method
  static kingdom() {
    return 'Mammalia';
  }
}

const dog = new Dog('Buddy', 'Golden Retriever');
console.log(dog.speak()); // "Buddy barks"
console.log(dog.fetch()); // "Buddy fetches the ball"
console.log(Dog.kingdom()); // "Mammalia"

Object Patterns

Common object patterns and best practices in JavaScript.

Factory Pattern

// Factory function
function createUser(name, email, role = 'user') {
  return {
    name,
    email,
    role,
    login() {
      console.log(`${this.name} logged in`);
    },
    logout() {
      console.log(`${this.name} logged out`);
    }
  };
}

const user = createUser('John', 'john@example.com');
user.login(); // "John logged in"

// Factory with closure
function createCounter() {
  let count = 0;
  
  return {
    increment() {
      return ++count;
    },
    decrement() {
      return --count;
    },
    reset() {
      count = 0;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2

Module Pattern

// Module pattern with IIFE
const Calculator = (function() {
  // Private variables
  let history = [];
  
  // Private function
  function addToHistory(operation, result) {
    history.push({ operation, result, timestamp: Date.now() });
  }
  
  // Public API
  return {
    add(a, b) {
      const result = a + b;
      addToHistory(`${a} + ${b}`, result);
      return result;
    },
    
    subtract(a, b) {
      const result = a - b;
      addToHistory(`${a} - ${b}`, result);
      return result;
    },
    
    getHistory() {
      return [...history]; // Return copy
    },
    
    clearHistory() {
      history = [];
    }
  };
})();

console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.getHistory()); // [{operation: "5 + 3", result: 8, ...}]

Observer Pattern

// Observer pattern
class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }
  
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(...args));
    }
  }
  
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
}

// Usage
const emitter = new EventEmitter();

emitter.on('data', data => {
  console.log('Received data:', data);
});

emitter.on('error', error => {
  console.error('Error:', error);
});

emitter.emit('data', { id: 1, name: 'Test' });
emitter.emit('error', new Error('Something went wrong'));

Modern Object Features

Recent additions to JavaScript's object capabilities.

Private Fields and Methods

// Private fields (ES2022)
class BankAccount {
  #balance = 0;
  #transactions = [];
  
  constructor(initialBalance = 0) {
    this.#balance = initialBalance;
  }
  
  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      this.#transactions.push({ type: 'deposit', amount, date: new Date() });
      return this.#balance;
    }
    throw new Error('Amount must be positive');
  }
  
  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      this.#transactions.push({ type: 'withdraw', amount, date: new Date() });
      return this.#balance;
    }
    throw new Error('Invalid withdrawal amount');
  }
  
  get balance() {
    return this.#balance;
  }
  
  get transactionHistory() {
    return [...this.#transactions]; // Return copy
  }
}

const account = new BankAccount(1000);
account.deposit(500);
console.log(account.balance); // 1500
// account.#balance = 10000; // SyntaxError - private field

Object Destructuring

// Object destructuring
const person = {
  firstName: 'John',
  lastName: 'Doe',
  age: 30,
  city: 'New York',
  country: 'USA'
};

// Basic destructuring
const { firstName, lastName, age } = person;
console.log(firstName, lastName, age); // "John", "Doe", 30

// With default values
const { firstName: first, age: years = 25 } = person;

// Rename and default
const { city: location = 'Unknown' } = person;

// Rest operator
const { firstName, lastName, ...rest } = person;
console.log(rest); // { age: 30, city: 'New York', country: 'USA' }

// Nested destructuring
const data = {
  user: {
    name: 'John',
    address: {
      street: '123 Main St',
      city: 'New York'
    }
  }
};

const { user: { name, address: { city } } } = data;
console.log(name, city); // "John", "New York"

// Function parameter destructuring
function greetPerson({ firstName, lastName, age = 25 }) {
  console.log(`Hello ${firstName} ${lastName}, age ${age}`);
}

greetPerson(person); // "Hello John Doe, age 30"

Optional Chaining

// Optional chaining with objects
const user = {
  name: 'John',
  address: {
    street: '123 Main St',
    city: 'New York'
  }
};

// Safe property access
console.log(user.address?.city); // "New York"
console.log(user.contact?.phone); // undefined

// Safe method calls
console.log(user.getAddress?.()); // undefined if method doesn't exist

// Optional chaining with destructuring
const { address: { city } = {} } = user;
console.log(city); // "New York"

// Deep optional chaining
const data = {
  level1: {
    level2: {
      level3: {
        value: 'found'
      }
    }
  }
};

console.log(data.level1?.level2?.level3?.value); // "found"
console.log(data.level1?.level2?.level4?.value); // undefined

Object Performance

Performance considerations when working with objects.

Property Access Performance

// Property access patterns
const obj = { a: 1, b: 2, c: 3 };

// Fast: Direct property access
obj.a;
obj['a'];

// Slower: Computed property access
const prop = 'a';
obj[prop];

// Slower: Prototype chain lookup
obj.toString; // Needs to check prototype

// Cache frequently accessed properties
function processObject(obj) {
  const value = obj.expensiveProperty; // Cache if used multiple times
  // Use value multiple times
}

// Use object literals for creation (faster than Object.create)
const fast = { a: 1, b: 2 };
const slow = Object.create({ a: 1, b: 2 });

Memory Considerations

// Object creation and garbage collection
function createObjects(count) {
  const objects = [];
  for (let i = 0; i < count; i++) {
    objects.push({ id: i, data: `item${i}` });
  }
  return objects;
}

// Reuse objects to reduce garbage collection
class ObjectPool {
  constructor(createFn, resetFn) {
    this.createFn = createFn;
    this.resetFn = resetFn;
    this.pool = [];
  }
  
  get() {
    if (this.pool.length > 0) {
      return this.pool.pop();
    }
    return this.createFn();
  }
  
  release(obj) {
    this.resetFn(obj);
    this.pool.push(obj);
  }
}

// Avoid unnecessary object creation
// Bad: Creating objects in loops
function bad(items) {
  return items.map(item => ({ ...item, processed: true }));
}

// Good: Modify in place if possible
function good(items) {
  return items.map(item => {
    item.processed = true;
    return item;
  });
}

Best Practices

Guidelines for writing clean and maintainable object-oriented code.

Object Design

// Keep objects focused and single-purpose
// Bad: God object
const godObject = {
  userData: {},
  uiElements: {},
  apiCalls: {},
  calculations: {},
  // ... hundreds of methods
};

// Good: Focused objects
class UserManager {
  constructor() {
    this.users = new Map();
  }
  
  addUser(user) { /* ... */ }
  removeUser(id) { /* ... */ }
  getUser(id) { /* ... */ }
}

class UIManager {
  constructor() {
    this.elements = new Map();
  }
  
  addElement(id, element) { /* ... */ }
  removeElement(id) { /* ... */ }
  getElement(id) { /* ... */ }
}

// Use composition over inheritance
// Bad: Deep inheritance chains
class Animal extends LivingThing { }
class Mammal extends Animal { }
class Dog extends Mammal { }
class GoldenRetriever extends Dog { }

// Good: Composition
class Dog {
  constructor(breed, behaviors) {
    this.breed = breed;
    this.behaviors = behaviors;
  }
  
  bark() {
    this.behaviors.makeSound('bark');
  }
}

Immutability

// Prefer immutable operations
// Bad: Mutating objects
function addItem(user, item) {
  user.items.push(item); // Mutates original
  return user;
}

// Good: Creating new objects
function addItem(user, item) {
  return {
    ...user,
    items: [...user.items, item]
  };
}

// Use Object.freeze for constants
const CONFIG = Object.freeze({
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
});

// Use immutable data structures for complex state
class ImmutableState {
  constructor(initialState) {
    this.state = Object.freeze({ ...initialState });
  }
  
  update(updates) {
    return new ImmutableState({ ...this.state, ...updates });
  }
  
  get(key) {
    return this.state[key];
  }
}

Error Handling

// Validate object properties
function createUser(userData) {
  if (!userData || typeof userData !== 'object') {
    throw new TypeError('userData must be an object');
  }
  
  const { name, email, age } = userData;
  
  if (!name || typeof name !== 'string') {
    throw new Error('name is required and must be a string');
  }
  
  if (!email || !email.includes('@')) {
    throw new Error('valid email is required');
  }
  
  if (age !== undefined && (typeof age !== 'number' || age < 0)) {
    throw new Error('age must be a positive number');
  }
  
  return { name, email, age };
}

// Safe property access
function getProperty(obj, path, defaultValue = undefined) {
  const keys = path.split('.');
  let current = obj;
  
  for (const key of keys) {
    if (current === null || current === undefined || !(key in current)) {
      return defaultValue;
    }
    current = current[key];
  }
  
  return current;
}

// Usage
const user = { name: 'John', address: { city: 'New York' } };
console.log(getProperty(user, 'address.city', 'Unknown')); // "New York"
console.log(getProperty(user, 'address.zip', 'Unknown')); // "Unknown"

Summary

Key Takeaways

  • Objects are fundamental data structures in JavaScript
  • Multiple ways to create objects: literals, constructors, classes
  • Prototype-based inheritance enables code reuse
  • ES6 classes provide cleaner syntax for OOP
  • Modern features include private fields and optional chaining

Best Practices

  • Use object literals for simple objects
  • Prefer composition over deep inheritance
  • Use immutable patterns for state management
  • Validate object properties and handle errors
  • Choose appropriate property access methods

Common Pitfalls

  • Modifying objects during iteration
  • Using for...in for arrays instead of objects
  • Creating unnecessary objects in loops
  • Not handling undefined property access
  • Overusing inheritance when composition is better