Technology · JavaScript

Intermediate

JavaScript Classes

Build objects with class syntax covering constructors, inheritance, static members, and private fields.

TL;DR
  1. 01Define object blueprints with class and constructor syntax.
  2. 02Inherit shared behavior using extends and super calls.
  3. 03Hide internal state with private fields marked by hash.

Class Basics

  • Use class and constructor to define a blueprint for creating objects with shared methods.
    class User {
      constructor(name, email) {
        this.name = name;
        this.email = email;
      }
    }
    
  • Create new instances with the new keyword, which runs the constructor automatically.
    const user = new User('Ana', 'ana@example.com');
    console.log(user.name); // "Ana"
    
  • Define instance methods inside the class body without the function keyword.
    class User {
      constructor(name) {
        this.name = name;
      }
      greet() {
        return `Hi, ${this.name}`;
      }
    }
    
  • Methods live on the prototype, so every instance shares one copy instead of duplicating code.
    const a = new User('Ana');
    const b = new User('Leo');
    console.log(a.greet === b.greet); // true
    
  • Class declarations run in strict mode automatically, which catches more silent bugs.
    class Demo {
      constructor() {
        undeclaredVar = 1; // throws ReferenceError in strict mode
      }
    }
    
  • Classes are not hoisted the way function declarations are, so define them before use.
    // new Greeter() here would throw a ReferenceError
    class Greeter {}
    

Static Methods and Properties

  • Mark a method static to attach it to the class itself instead of each instance.
    class MathHelper {
      static square(n) {
        return n * n;
      }
    }
    MathHelper.square(4); // 16
    
  • Static methods cannot access instance data through this because no instance exists.
    class Counter {
      static count = 0;
      constructor() {
        Counter.count++;
      }
    }
    
  • Use static properties to track data shared across all instances, like a running total.
    new Counter();
    new Counter();
    console.log(Counter.count); // 2
    
  • Build factory methods as static functions that return configured instances.
    class User {
      static fromJSON(json) {
        const data = JSON.parse(json);
        return new User(data.name, data.email);
      }
    }
    
  • Static blocks let you run setup logic once when the class is first defined.
    class Config {
      static settings;
      static {
        Config.settings = loadDefaults();
      }
    }
    

Getters, Setters, and Private Fields

  • Use get to define a property that computes its value each time it's read.
    class Circle {
      constructor(radius) {
        this.radius = radius;
      }
      get area() {
        return Math.PI * this.radius ** 2;
      }
    }
    
  • Use set to run logic, like validation, whenever a property is assigned.
    class Circle {
      set radius(value) {
        if (value <= 0) throw new RangeError('Radius must be positive');
        this._radius = value;
      }
    }
    
  • Mark fields private with a leading # so they can't be read or changed outside the class.
    class BankAccount {
      #balance = 0;
      deposit(amount) {
        this.#balance += amount;
      }
      get balance() {
        return this.#balance;
      }
    }
    
  • Accessing a private field from outside the class throws a SyntaxError, not just undefined.
    const acc = new BankAccount();
    acc.#balance; // SyntaxError: Private field must be declared in an enclosing class
    
  • Private methods work the same way, hiding internal logic from the public API.
    class Order {
      #calculateTax(amount) {
        return amount * 0.08;
      }
      getTotal(amount) {
        return amount + this.#calculateTax(amount);
      }
    }
    

Inheritance with Extends and Super

  • Use extends to create a subclass that inherits methods and properties from a parent.
    class Animal {
      constructor(name) {
        this.name = name;
      }
      speak() {
        return `${this.name} makes a sound`;
      }
    }
    class Dog extends Animal {}
    
  • Call super() inside a subclass constructor to run the parent constructor first.
    class Dog extends Animal {
      constructor(name, breed) {
        super(name);
        this.breed = breed;
      }
    }
    
  • Override a parent method by redefining it with the same name in the subclass.
    class Dog extends Animal {
      speak() {
        return `${this.name} barks`;
      }
    }
    
  • Call super.methodName() to reuse parent logic instead of duplicating it.
    class Dog extends Animal {
      speak() {
        return `${super.speak()} loudly`;
      }
    }
    
  • Use instanceof to check whether an object inherits from a given class.
    const rex = new Dog('Rex', 'Lab');
    console.log(rex instanceof Animal); // true
    

Classes vs Prototypes

  • Classes are syntactic sugar over JavaScript's existing prototype-based inheritance model.
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    }
    // Roughly equivalent to a constructor function + prototype assignment
    
  • A class method becomes a non-enumerable property on the constructor's prototype object.
    class Point {
      distanceTo(other) {
        return Math.hypot(this.x - other.x, this.y - other.y);
      }
    }
    console.log(typeof Point.prototype.distanceTo); // "function"
    
  • The typeof a class is still "function", confirming classes are functions under the hood.
    console.log(typeof Point); // "function"
    
  • Unlike old-style constructor functions, classes throw an error if called without new.
    function OldStyle() {}
    OldStyle(); // works (but usually a bug)
    
    class NewStyle {}
    NewStyle(); // TypeError: Class constructor cannot be invoked without 'new'
    
Tips
  1. 01Use private fields with a hash prefix to stop outside code from reading or changing internal state directly.
  2. 02Call super() before using this in a subclass constructor, since the parent must initialize the instance first.
  3. 03Prefer static methods for utility functions that relate to a class but don't need a specific instance.
Warnings
  1. 01Forgetting to call super() in a subclass constructor throws a ReferenceError before this can be accessed.
  2. 02Arrow function class fields capture this permanently, which can surprise developers expecting normal method binding rules.
  3. 03Class declarations are not hoisted like functions, so using a class before its definition throws an error.
FAQ
JavaScript Call Apply BindJavaScript Currying and Composition