Skip to content

JavaScript Classes and Objects

Classes and objects are core concepts of Object-Oriented Programming (OOP). JavaScript introduced class syntax starting from ES6, making object-oriented programming more intuitive and easier to understand. In this chapter, we will learn about classes and objects in JavaScript.

What is Object-Oriented Programming

Object-Oriented Programming (OOP) is a programming paradigm that uses objects to organize code. The core concepts of OOP include:

  1. Encapsulation: Combining data and methods that operate on the data
  2. Inheritance: Child classes can inherit properties and methods from parent classes
  3. Polymorphism: Same interface can have different implementations
  4. Abstraction: Hiding complex implementation details, exposing only necessary interfaces

Object Basics

Object Literals

In JavaScript, objects are collections of properties and methods:

javascript
// Object literal
const person = {
  name: "John",
  age: 25,
  greet: function() {
    return `Hello, I'm ${this.name}`;
  }
};

console.log(person.name); // "John"
console.log(person.greet()); // "Hello, I'm John"

Object Property Access

javascript
const user = {
  name: "Jane",
  "full-name": "Jane Doe",
  age: 30
};

// Dot notation
console.log(user.name); // "Jane"

// Bracket notation
console.log(user["full-name"]); // "Jane Doe"
console.log(user["age"]); // 30

// Dynamic property access
const propertyName = "name";
console.log(user[propertyName]); // "Jane"

Object Methods

javascript
const calculator = {
  result: 0,
  
  add: function(num) {
    this.result += num;
    return this;
  },
  
  subtract: function(num) {
    this.result -= num;
    return this;
  },
  
  getResult: function() {
    return this.result;
  },
  
  reset: function() {
    this.result = 0;
    return this;
  }
};

// Method chaining
const result = calculator.add(10).subtract(3).getResult();
console.log(result); // 7

ES6 Class Syntax

Basic Class Definition

javascript
// Define a simple class
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `Hello, I'm ${this.name}, ${this.age} years old`;
  }
  
  // Static method
  static getSpecies() {
    return "Human";
  }
}

// Create class instances
const person1 = new Person("John", 25);
const person2 = new Person("Jane", 30);

console.log(person1.greet()); // "Hello, I'm John, 25 years old"
console.log(Person.getSpecies()); // "Human"

Constructor

javascript
class Rectangle {
  constructor(width, height) {
    // Parameter validation
    if (width <= 0 || height <= 0) {
      throw new Error("Width and height must be greater than 0");
    }
    
    this.width = width;
    this.height = height;
  }
  
  // Calculate area
  getArea() {
    return this.width * this.height;
  }
  
  // Calculate perimeter
  getPerimeter() {
    return 2 * (this.width + this.height);
  }
  
  // Get description
  getDescription() {
    return `This is a ${this.width}x${this.height} rectangle`;
  }
}

const rect = new Rectangle(10, 5);
console.log(rect.getArea()); // 50
console.log(rect.getPerimeter()); // 30

Getter and Setter

javascript
class User {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this._age = 0; // Private property convention
  }
  
  // Getter
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
  
  get age() {
    return this._age;
  }
  
  // Setter
  set age(value) {
    if (value < 0) {
      throw new Error("Age cannot be negative");
    }
    this._age = value;
  }
  
  // Read-only property
  get species() {
    return "Human";
  }
}

const user = new User("John", "Doe");
console.log(user.fullName); // "John Doe"

user.age = 25;
console.log(user.age); // 25

Private Properties and Methods (ES2022)

javascript
class BankAccount {
  // Private properties
  #balance = 0;
  #accountNumber;
  
  constructor(accountNumber, initialBalance = 0) {
    this.#accountNumber = accountNumber;
    this.#balance = initialBalance;
  }
  
  // Public methods
  deposit(amount) {
    if (amount <= 0) {
      throw new Error("Deposit amount must be greater than 0");
    }
    this.#balance += amount;
    return this.#balance;
  }
  
  withdraw(amount) {
    if (amount <= 0) {
      throw new Error("Withdrawal amount must be greater than 0");
    }
    if (amount > this.#balance) {
      throw new Error("Insufficient balance");
    }
    this.#balance -= amount;
    return this.#balance;
  }
  
  getBalance() {
    return this.#balance;
  }
  
  // Private method
  #validateTransaction(amount) {
    return amount > 0 && amount <= this.#balance;
  }
  
  // Public method can call private method
  transfer(amount, targetAccount) {
    if (this.#validateTransaction(amount)) {
      this.withdraw(amount);
      targetAccount.deposit(amount);
      return true;
    }
    return false;
  }
}

const account1 = new BankAccount("123456", 1000);
const account2 = new BankAccount("789012", 500);

account1.deposit(200);
console.log(account1.getBalance()); // 1200

account1.withdraw(100);
console.log(account1.getBalance()); // 1100

Class Inheritance

Basic Inheritance

javascript
// Parent class
class Animal {
  constructor(name, species) {
    this.name = name;
    this.species = species;
  }
  
  speak() {
    return `${this.name} makes a sound`;
  }
  
  getInfo() {
    return `I'm a ${this.species} named ${this.name}`;
  }
}

// Child class
class Dog extends Animal {
  constructor(name, breed) {
    super(name, "Dog"); // Call parent constructor
    this.breed = breed;
  }
  
  // Override parent method
  speak() {
    return `${this.name} barks`;
  }
  
  // Child-specific method
  fetch() {
    return `${this.name} fetches the ball`;
  }
}

class Cat extends Animal {
  constructor(name, color) {
    super(name, "Cat");
    this.color = color;
  }
  
  speak() {
    return `${this.name} meows`;
  }
  
  climb() {
    return `${this.name} climbs the tree`;
  }
}

const dog = new Dog("Buddy", "Golden Retriever");
const cat = new Cat("Whiskers", "Orange");

console.log(dog.speak()); // "Buddy barks"
console.log(dog.fetch()); // "Buddy fetches the ball"
console.log(dog.getInfo()); // "I'm a Dog named Buddy"

console.log(cat.speak()); // "Whiskers meows"
console.log(cat.climb()); // "Whiskers climbs the tree"

Static Properties and Methods

javascript
class MathUtils {
  // Static property (ES2022)
  static PI = 3.14159;
  
  // Static methods
  static add(a, b) {
    return a + b;
  }
  
  static multiply(a, b) {
    return a * b;
  }
  
  static circleArea(radius) {
    return this.PI * radius * radius; // Use static property
  }
  
  static isEven(number) {
    return number % 2 === 0;
  }
}

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.circleArea(5)); // 78.53975
console.log(MathUtils.isEven(4)); // true

Object Prototype

Prototype Chain

javascript
// Constructor function (ES5 way)
function Person(name, age) {
  this.name = name;
  this.age = age;
}

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

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

const person = new Person("John", 25);
console.log(person.greet()); // "Hello, I'm John"
console.log(person.getAge()); // 25

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

Object.create()

javascript
// Create object using Object.create()
const animal = {
  species: "Animal",
  speak: function() {
    return `${this.name} makes a sound`;
  }
};

const dog = Object.create(animal);
dog.name = "Buddy";
dog.breed = "Golden Retriever";

console.log(dog.speak()); // "Buddy makes a sound"
console.log(dog.species); // "Animal"

// Create object without prototype
const obj = Object.create(null);
obj.name = "John";
// console.log(obj.toString); // undefined

Object Methods

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

javascript
const user = {
  name: "John",
  age: 25,
  city: "New York"
};

// Get all keys
console.log(Object.keys(user)); // ["name", "age", "city"]

// Get all values
console.log(Object.values(user)); // ["John", 25, "New York"]

// Get key-value pairs array
console.log(Object.entries(user)); 
// [["name", "John"], ["age", 25], ["city", "New York"]]

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

Object.assign()

javascript
// Object merging
const target = { a: 1, b: 2 };
const source1 = { b: 4, c: 5 };
const source2 = { c: 6, d: 7 };

const result = Object.assign(target, source1, source2);
console.log(result); // { a: 1, b: 4, c: 6, d: 7 }

// Shallow copy
const original = { name: "John", age: 25 };
const copy = Object.assign({}, original);
copy.age = 26;

console.log(original.age); // 25
console.log(copy.age); // 26

Object Destructuring

javascript
const user = {
  name: "John",
  age: 25,
  address: {
    city: "New York",
    district: "Manhattan"
  },
  hobbies: ["reading", "swimming"]
};

// Basic destructuring
const { name, age } = user;
console.log(name, age); // "John" 25

// Renaming
const { name: userName, age: userAge } = user;
console.log(userName, userAge); // "John" 25

// Default values
const { name: n, gender = "Unknown" } = user;
console.log(n, gender); // "John" "Unknown"

// Nested destructuring
const { address: { city, district } } = user;
console.log(city, district); // "New York" "Manhattan"

// Array destructuring
const { hobbies: [firstHobby, secondHobby] } = user;
console.log(firstHobby, secondHobby); // "reading" "swimming"

Design Patterns

Factory Pattern

javascript
class UserFactory {
  static createUser(type, name, ...args) {
    switch (type) {
      case "admin":
        return new AdminUser(name, ...args);
      case "customer":
        return new CustomerUser(name, ...args);
      case "guest":
        return new GuestUser(name);
      default:
        throw new Error("Unknown user type");
    }
  }
}

class User {
  constructor(name) {
    this.name = name;
    this.createdAt = new Date();
  }
  
  getInfo() {
    return `User: ${this.name}, Created: ${this.createdAt}`;
  }
}

class AdminUser extends User {
  constructor(name, permissions = []) {
    super(name);
    this.role = "admin";
    this.permissions = permissions;
  }
  
  getInfo() {
    return `${super.getInfo()}, Role: ${this.role}, Permissions: ${this.permissions.join(", ")}`;
  }
}

class CustomerUser extends User {
  constructor(name, customerId) {
    super(name);
    this.role = "customer";
    this.customerId = customerId;
  }
}

class GuestUser extends User {
  constructor(name) {
    super(name || "Guest");
    this.role = "guest";
  }
}

// Use factory to create different user types
const admin = UserFactory.createUser("admin", "Admin", ["read", "write", "delete"]);
const customer = UserFactory.createUser("customer", "Customer", "CUST001");
const guest = UserFactory.createUser("guest", "Guest");

console.log(admin.getInfo());
console.log(customer.getInfo());
console.log(guest.getInfo());

Singleton Pattern

javascript
class Logger {
  constructor() {
    if (Logger.instance) {
      return Logger.instance;
    }
    
    this.logs = [];
    this.level = "info";
    
    Logger.instance = this;
    return this;
  }
  
  static getInstance() {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
  
  setLevel(level) {
    this.level = level;
  }
  
  log(message, level = "info") {
    if (this.shouldLog(level)) {
      const logEntry = {
        timestamp: new Date().toISOString(),
        level: level,
        message: message
      };
      
      this.logs.push(logEntry);
      console.log(`[${logEntry.timestamp}] [${logEntry.level.toUpperCase()}] ${logEntry.message}`);
    }
  }
  
  info(message) {
    this.log(message, "info");
  }
  
  warn(message) {
    this.log(message, "warn");
  }
  
  error(message) {
    this.log(message, "error");
  }
  
  shouldLog(level) {
    const levels = { debug: 0, info: 1, warn: 2, error: 3 };
    return levels[level] >= levels[this.level];
  }
  
  getLogs() {
    return this.logs;
  }
}

// Use singleton
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();

console.log(logger1 === logger2); // true

logger1.info("This is the first log");
logger2.warn("This is the second log");
logger1.error("This is the third log");

console.log(logger1.getLogs().length); // 3
console.log(logger2.getLogs().length); // 3

Shopping Cart Example

javascript
class ShoppingCart {
  constructor() {
    this.items = [];
    this.discount = 0;
  }
  
  addItem(product, quantity = 1) {
    const existingItem = this.items.find(item => item.product.id === product.id);
    
    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      this.items.push({ product, quantity });
    }
    
    return this;
  }
  
  removeItem(productId) {
    this.items = this.items.filter(item => item.product.id !== productId);
    return this;
  }
  
  getTotalPrice() {
    const subtotal = this.items.reduce((total, item) => {
      return total + (item.product.price * item.quantity);
    }, 0);
    
    return subtotal * (1 - this.discount);
  }
  
  setDiscount(discount) {
    if (discount < 0 || discount > 1) {
      throw new Error("Discount must be between 0 and 1");
    }
    this.discount = discount;
    return this;
  }
  
  getItemsCount() {
    return this.items.reduce((count, item) => count + item.quantity, 0);
  }
  
  clear() {
    this.items = [];
    this.discount = 0;
    return this;
  }
}

// Usage example
const cart = new ShoppingCart();

const product1 = { id: 1, name: "Laptop", price: 999 };
const product2 = { id: 2, name: "Mouse", price: 29 };

cart.addItem(product1, 1)
    .addItem(product2, 2)
    .setDiscount(0.1); // 10% discount

console.log(cart.getTotalPrice()); // 951.3
console.log(cart.getItemsCount()); // 3

Summary

Key points about JavaScript classes and objects:

  1. Object Basics: Collections of properties and methods, created with literals or constructors
  2. ES6 Class Syntax: Use class keyword to define classes, constructor for initialization
  3. Getter and Setter: Special methods for accessing and setting properties
  4. Private Properties: Use # prefix for private properties and methods
  5. Inheritance: Use extends keyword for class inheritance, super to call parent
  6. Static Properties and Methods: Belong to the class itself, not instances
  7. Prototype Chain: The foundation of JavaScript object inheritance
  8. Object Methods: Object.keys(), Object.values(), Object.entries(), etc.
  9. Design Patterns: Factory pattern, singleton pattern, etc.

Mastering classes and objects is fundamental for learning object-oriented programming. In the next chapter, we will learn about JavaScript class inheritance.

Content is for learning and research only.