JavaScript Classes
Introduction
JavaScript classes, introduced in ECMAScript 2015 (ES6), provide a cleaner and more intuitive syntax for creating objects and implementing inheritance. Although JavaScript is primarily a prototype-based language, classes offer a more familiar, object-oriented approach for developers coming from languages like Java or C#.
Classes act as templates or blueprints for creating objects with predefined properties and methods. They encapsulate data and behavior, making your code more organized, reusable, and maintainable.
Class Syntax Basics
Let's start with the basic syntax of a JavaScript class:
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
  }
}
// Creating an instance
const john = new Person('John', 30);
console.log(john.greet()); // Output: Hello, my name is John and I am 30 years old.
In this example:
- We define a Personclass with a constructor and a method
- The constructormethod is called automatically when a new object is created
- We create a new instance with the newkeyword
- We can call the greet()method on our instance
Constructor Method
The constructor method is a special method that:
- Gets called automatically when an object is instantiated
- Is used to initialize object properties
- Can accept parameters to set initial values
class Book {
  constructor(title, author, year) {
    this.title = title;
    this.author = author;
    this.year = year;
    this.isAvailable = true; // Default property
  }
}
const myBook = new Book('JavaScript: The Good Parts', 'Douglas Crockford', 2008);
console.log(myBook);
/* Output:
Book {
  title: 'JavaScript: The Good Parts',
  author: 'Douglas Crockford',
  year: 2008,
  isAvailable: true
}
*/
Class Methods
Methods in classes are functions that are associated with the class. They're defined without the function keyword.
class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // Method to calculate area
  calculateArea() {
    return this.height * this.width;
  }
  // Method to calculate perimeter
  calculatePerimeter() {
    return 2 * (this.height + this.width);
  }
  
  // Method with parameters
  scale(factor) {
    this.height *= factor;
    this.width *= factor;
    return this;
  }
}
const rect = new Rectangle(5, 10);
console.log(rect.calculateArea()); // Output: 50
console.log(rect.calculatePerimeter()); // Output: 30
// Method chaining
rect.scale(2);
console.log(rect.calculateArea()); // Output: 200 (after scaling)
Getters and Setters
JavaScript classes can have getter and setter methods that let you get and set values, with added logic if needed:
class Temperature {
  constructor(celsius) {
    this._celsius = celsius;
  }
  
  // Getter for celsius
  get celsius() {
    return this._celsius;
  }
  
  // Setter for celsius
  set celsius(value) {
    if (value < -273.15) {
      throw new Error('Temperature below absolute zero is not possible');
    }
    this._celsius = value;
  }
  
  // Getter for fahrenheit
  get fahrenheit() {
    return this._celsius * 9/5 + 32;
  }
  
  // Setter for fahrenheit
  set fahrenheit(value) {
    this.celsius = (value - 32) * 5/9;
  }
}
const temp = new Temperature(25);
console.log(temp.celsius); // Output: 25
console.log(temp.fahrenheit); // Output: 77
temp.celsius = 30;
console.log(temp.fahrenheit); // Output: 86
temp.fahrenheit = 68;
console.log(temp.celsius); // Output: 20
Notice how we use an underscore prefix (_celsius) for the internal property. This is a convention to indicate that the property should be accessed via getters and setters, not directly.
Static Methods and Properties
Static methods and properties belong to the class itself, not to instances of the class. They're useful for utility functions related to the class.
class MathUtils {
  static PI = 3.14159;
  
  static square(x) {
    return x * x;
  }
  
  static cube(x) {
    return x * x * x;
  }
}
console.log(MathUtils.PI); // Output: 3.14159
console.log(MathUtils.square(4)); // Output: 16
console.log(MathUtils.cube(3)); // Output: 27
// This would throw an error:
// const math = new MathUtils();
// console.log(math.square(4));
Class Inheritance
One of the most powerful features of classes is inheritance, which allows a class to inherit properties and methods from another class:
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    return `${this.name} makes a noise.`;
  }
}
class Dog extends Animal {
  constructor(name, breed) {
    // Call the parent constructor
    super(name);
    this.breed = breed;
  }
  
  // Override the speak method
  speak() {
    return `${this.name} barks! Woof woof!`;
  }
  
  // Add a new method
  fetch() {
    return `${this.name} fetches the ball!`;
  }
}
const dog = new Dog('Rex', 'German Shepherd');
console.log(dog.name); // Output: Rex
console.log(dog.breed); // Output: German Shepherd
console.log(dog.speak()); // Output: Rex barks! Woof woof!
console.log(dog.fetch()); // Output: Rex fetches the ball!
Key points about inheritance:
- Use the extendskeyword to create a subclass
- Use super()in the constructor to call the parent class constructor
- Subclasses can override methods from the parent class
- Subclasses can add new methods and properties
Private Fields and Methods
As of more recent JavaScript versions, you can create truly private class fields using the # prefix:
class BankAccount {
  #balance = 0;
  #accountNumber;
  
  constructor(accountHolder, initialBalance) {
    this.accountHolder = accountHolder;
    this.#accountNumber = this.#generateAccountNumber();
    
    if (initialBalance > 0) {
      this.deposit(initialBalance);
    }
  }
  
  #generateAccountNumber() {
    return Math.floor(Math.random() * 1000000000);
  }
  
  get balance() {
    return this.#balance;
  }
  
  get accountDetails() {
    return {
      holder: this.accountHolder,
      accountNumber: this.#accountNumber,
      balance: this.#balance
    };
  }
  
  deposit(amount) {
    if (amount <= 0) throw new Error('Invalid deposit amount');
    this.#balance += amount;
    return this.#balance;
  }
  
  withdraw(amount) {
    if (amount <= 0) throw new Error('Invalid withdrawal amount');
    if (amount > this.#balance) throw new Error('Insufficient funds');
    
    this.#balance -= amount;
    return this.#balance;
  }
}
const account = new BankAccount('Alice Smith', 1000);
console.log(account.balance); // Output: 1000
console.log(account.accountDetails); // Output: {holder: 'Alice Smith', accountNumber: (some number), balance: 1000}
account.deposit(500);
console.log(account.balance); // Output: 1500
account.withdraw(200);
console.log(account.balance); // Output: 1300
// These would throw errors:
// console.log(account.#balance);
// console.log(account.#accountNumber);
// account.#generateAccountNumber();
Private fields and methods are not accessible outside the class, providing true encapsulation.
Real-World Example: Shopping Cart
Let's create a more comprehensive example that models a shopping cart system:
class Product {
  constructor(id, name, price) {
    this.id = id;
    this.name = name;
    this.price = price;
  }
  
  getFormattedPrice() {
    return `$${this.price.toFixed(2)}`;
  }
}
class CartItem {
  constructor(product, quantity = 1) {
    this.product = product;
    this.quantity = quantity;
  }
  
  getTotalPrice() {
    return this.product.price * this.quantity;
  }
}
class ShoppingCart {
  #items = [];
  
  addItem(product, quantity = 1) {
    const existingItem = this.#items.find(item => item.product.id === product.id);
    
    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      this.#items.push(new CartItem(product, quantity));
    }
  }
  
  removeItem(productId) {
    const index = this.#items.findIndex(item => item.product.id === productId);
    if (index !== -1) {
      this.#items.splice(index, 1);
      return true;
    }
    return false;
  }
  
  updateQuantity(productId, quantity) {
    if (quantity <= 0) {
      return this.removeItem(productId);
    }
    
    const item = this.#items.find(item => item.product.id === productId);
    if (item) {
      item.quantity = quantity;
      return true;
    }
    return false;
  }
  
  getItems() {
    return [...this.#items];
  }
  
  getTotalPrice() {
    return this.#items.reduce((total, item) => total + item.getTotalPrice(), 0);
  }
  
  getItemCount() {
    return this.#items.reduce((count, item) => count + item.quantity, 0);
  }
  
  clear() {
    this.#items = [];
  }
}
// Usage example:
const laptop = new Product(1, 'Laptop', 999.99);
const phone = new Product(2, 'Smartphone', 699.99);
const headphones = new Product(3, 'Headphones', 149.99);
const cart = new ShoppingCart();
cart.addItem(laptop);
cart.addItem(phone, 2);
cart.addItem(headphones, 3);
console.log('Cart items:', cart.getItems());
console.log('Total price:', cart.getTotalPrice().toFixed(2)); // Expected: 2999.94
console.log('Item count:', cart.getItemCount()); // Expected: 6
cart.updateQuantity(2, 1); // Update phone quantity to 1
console.log('Updated item count:', cart.getItemCount()); // Expected: 5
console.log('Updated total price:', cart.getTotalPrice().toFixed(2)); // Expected: 2299.95
cart.removeItem(3); // Remove headphones
console.log('Final item count:', cart.getItemCount()); // Expected: 2
console.log('Final total price:', cart.getTotalPrice().toFixed(2)); // Expected: 1699.98
This example demonstrates many features of classes working together:
- Multiple classes interacting (Product, CartItem, ShoppingCart)
- Private fields (the cart's items array)
- Methods with different responsibilities
- Data encapsulation
Summary
JavaScript classes provide a clean, intuitive syntax for creating object-oriented code in JavaScript. They offer:
- A structured way to create objects with consistent properties and methods
- Constructor functions for setting up new objects
- Instance methods for object-specific functionality
- Static methods and properties for class-level utilities
- Inheritance for creating hierarchies of related classes
- Private fields and methods for proper encapsulation
- Getters and setters for controlled property access
While JavaScript's class syntax is syntactic sugar over its prototype-based inheritance, it provides a more familiar pattern for developers from traditional object-oriented backgrounds and helps organize code in larger applications.
Exercises
- 
Create a Circleclass with properties for radius and methods to calculate area and circumference.
- 
Create a Userclass with properties like username and email, then extend it to create anAdminclass with additional permissions.
- 
Build a simple library system with Book,Author, andLibraryclasses that interact with each other.
- 
Implement a Timerclass that can start, stop, reset, and report elapsed time.
- 
Create a Vehicleclass hierarchy with different types of vehicles that inherit from a base class.
Additional Resources
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!