JavaScript Classes

JavaScript classes, introduced in ECMAScript 2015 (ES6), provide a cleaner and more elegant syntax for creating objects and dealing with inheritance. While JavaScript remains prototype-based, classes offer a more familiar syntax for developers coming from class-based languages.

Class Basics

A JavaScript class is a type of function, but instead of using the function keyword, we use the class keyword and the properties are assigned inside a constructor() method.

// Class declaration
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  // Method
  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()); // "Hello, my name is John and I am 30 years old."

Note: Class declarations are not hoisted. You need to declare a class before you can use it.

Class Expressions

Similar to function expressions, classes can also be defined using expressions:

// Unnamed class expression
const Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
  }
};

// Named class expression
const Employee = class EmployeeClass {
  constructor(name, position) {
    this.name = name;
    this.position = position;
  }
};

Class Methods

Classes can contain several types of methods:

Constructor Method

The constructor method is a special method for creating and initializing objects created with a class. There can only be one constructor method in a class.

Instance Methods

Instance methods are functions that are available on instances of the class:

class Calculator {
  add(a, b) {
    return a + b;
  }
  
  subtract(a, b) {
    return a - b;
  }
}

const calc = new Calculator();
console.log(calc.add(5, 3));      // 8
console.log(calc.subtract(10, 4)); // 6

Static Methods

Static methods are called on the class itself, not on instances of 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);        // 3.14159
console.log(MathUtils.square(4)); // 16
console.log(MathUtils.cube(3));   // 27

// This would throw an error:
// const math = new MathUtils();
// console.log(math.square(4));

Getter and Setter Methods

Classes also support getter and setter methods, which allow you to control access to class properties:

class Temperature {
  constructor(celsius) {
    this._celsius = celsius;
  }
  
  get celsius() {
    return this._celsius;
  }
  
  set celsius(value) {
    if (value < -273.15) {
      throw new Error('Temperature below absolute zero is not possible');
    }
    this._celsius = value;
  }
  
  get fahrenheit() {
    return this._celsius * 9/5 + 32;
  }
  
  set fahrenheit(value) {
    this.celsius = (value - 32) * 5/9;
  }
}

const temp = new Temperature(25);
console.log(temp.celsius);    // 25
console.log(temp.fahrenheit); // 77

temp.celsius = 30;
console.log(temp.fahrenheit); // 86

temp.fahrenheit = 68;
console.log(temp.celsius);    // 20

Class Inheritance

Classes can inherit from other classes using the extends keyword:

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 parent method
  speak() {
    return `${this.name} barks!`;
  }
  
  // Add a new method
  getBreed() {
    return `${this.name} is a ${this.breed}.`;
  }
}

const dog = new Dog('Rex', 'German Shepherd');
console.log(dog.speak());   // "Rex barks!"
console.log(dog.getBreed()); // "Rex is a German Shepherd."

Note: The super keyword is used to call the constructor of the parent class. If you use the this keyword in a constructor, you must call super() first.

Private Class Features

Modern JavaScript (ES2022) supports private class fields and methods using the # prefix:

 0) {
      this.#deposit(initialBalance);
    }
  }
  
  // Private method
  #deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      return true;
    }
    return false;
  }
  
  // Public methods
  deposit(amount) {
    return this.#deposit(amount);
  }
  
  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      return true;
    }
    return false;
  }
  
  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount('John Doe', 1000);
console.log(account.getBalance()); // 1000

account.deposit(500);
console.log(account.getBalance()); // 1500

account.withdraw(200);
console.log(account.getBalance()); // 1300

// This would throw an error:
// console.log(account.#balance);
// account.#deposit(100);]]>

Warning: Private class features are a relatively new addition to JavaScript. They may not be supported in older browsers without transpilation.

Implementing Interfaces

JavaScript doesn't have built-in interfaces like TypeScript or other strongly-typed languages. However, you can implement interface-like patterns:

// Define an "interface" as a set of methods a class should implement
const ShapeInterface = {
  calculateArea() {},
  calculatePerimeter() {}
};

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  
  calculateArea() {
    return this.width * this.height;
  }
  
  calculatePerimeter() {
    return 2 * (this.width + this.height);
  }
}

class Circle {
  constructor(radius) {
    this.radius = radius;
  }
  
  calculateArea() {
    return Math.PI * this.radius * this.radius;
  }
  
  calculatePerimeter() {
    return 2 * Math.PI * this.radius;
  }
}

// Function that works with any "shape" that implements the interface
function printShapeInfo(shape) {
  console.log(`Area: ${shape.calculateArea()}`);
  console.log(`Perimeter: ${shape.calculatePerimeter()}`);
}

const rect = new Rectangle(5, 10);
const circle = new Circle(7);

printShapeInfo(rect);
printShapeInfo(circle);

Mixins

Mixins are a way to add methods to classes without inheritance:

// Mixin
const SpeakerMixin = {
  speak(phrase) {
    console.log(`${this.name} says: ${phrase}`);
  }
};

const SwimmerMixin = {
  swim() {
    console.log(`${this.name} is swimming.`);
  }
};

// Base class
class Person {
  constructor(name) {
    this.name = name;
  }
}

// Apply mixins
Object.assign(Person.prototype, SpeakerMixin);
Object.assign(Person.prototype, SwimmerMixin);

const person = new Person('John');
person.speak('Hello!');  // "John says: Hello!"
person.swim();           // "John is swimming."

Interactive Example

Try out this interactive example to see JavaScript classes in action:

Best Practices

  • Keep classes focused: Each class should have a single responsibility.
  • Use private fields: Encapsulate internal state using private fields when possible.
  • Prefer composition over inheritance: Deep inheritance hierarchies can be difficult to maintain.
  • Document your classes: Use JSDoc comments to document the purpose and usage of your classes.
  • Be consistent: Follow a consistent naming convention for your classes and methods.

Next Steps

Now that you understand JavaScript classes, you might want to explore:

  • Design Patterns in JavaScript
  • TypeScript Classes and Interfaces
  • Web Components
  • Object-Oriented Programming Principles