Classes

Learn ES6 class syntax — constructors, methods, fields, static members, getters, setters — and how extends/super implement inheritance on top of the prototype system.

ES6 introduced class syntax as a cleaner, more familiar way to write object-oriented code in JavaScript. It's important to understand that classes are 'syntactic sugar': under the hood they still use the exact same prototype chain from the previous lesson — class syntax just makes that mechanism much easier to write and read.

Defining a class: constructor and methods

A class is declared with the class keyword, and its constructor method runs automatically whenever a new instance is created with new ClassName(...). Inside the constructor (and any method), this refers to the specific instance being created or operated on.

Any method defined directly in the class body (outside the constructor) is automatically added to the class's .prototype, meaning it's shared by every instance rather than duplicated on each one — exactly matching the manual pattern from the previous lesson, but far more concise.

Class fields

Class fields let you declare instance properties directly in the class body, without needing the constructor: class Counter { count = 0; }. This is shorthand for setting this.count = 0; inside the constructor, and it runs for every new instance automatically, before the constructor body executes.

Class fields are especially useful for initializing values that don't depend on constructor parameters, and for defining arrow-function methods that need a permanently bound this (common in UI event handlers).

Static members

Members marked static belong to the class itself, not to individual instances — you call them as ClassName.method(), not instance.method(). Static members are useful for utility functions related to the class, or for tracking data shared across all instances (like a running count of how many instances have been created).

Getters and setters

get and set define accessor methods that look like plain properties from the outside but run code behind the scenes. A getter (get fullName() { ... }) lets you compute a value on access (instance.fullName) as if it were a normal property, without parentheses. A setter (set fullName(value) { ... }) lets you intercept and validate an assignment (instance.fullName = "...").

Inheritance: extends and super

extends establishes a subclass relationship: class Dog extends Animal { } makes Dog inherit Animal's methods (via the prototype chain, exactly as in the previous lesson) while letting Dog add or override its own.

Inside a subclass constructor, super(...) must be called before using this, and it invokes the parent class's constructor — this is what actually initializes the inherited part of the instance. super.methodName() (used in a regular method, not the constructor) calls the parent class's version of an overridden method, letting a subclass extend rather than fully replace parent behavior.

Quick reference

Class declarations are not hoisted the way function declarations are — you must define a class before using it. A subclass constructor must call super(...) before accessing this, or JavaScript throws a ReferenceError. Static members are accessed via the class name (Animal.kingdom), never via an instance (rex.kingdom is undefined).

class Animal {
  static kingdom = "Animalia"; // static field
  #energy = 100;                // private field (see next lesson)

  constructor(name) {
    this.name = name;           // instance field via constructor
  }

  speak() {                     // instance method (shared via prototype)
    return `${this.name} makes a sound.`;
  }

  get energy() {                // getter
    return this.#energy;
  }
  set energy(value) {           // setter
    this.#energy = Math.max(0, value);
  }

  static create(name) {         // static method
    return new Animal(name);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);                // must run before using `this`
    this.breed = breed;
  }
  speak() {
    return `${super.speak()} Specifically, ${this.name} barks.`;
  }
}

const rex = new Dog("Rex", "Labrador");
console.log(rex.speak());
console.log(rex instanceof Animal); // true
console.log(Animal.kingdom);        // "Animalia" — accessed on the class, not an instance

Code Examples

This is the simplest possible class: a constructor that sets two instance properties, and a single method that computes something from them. new Rectangle(4, 5) runs the constructor with width=4, height=5. Expected result: 20 area() { return this.width * this.height; } is a shared method placed on Rectangle.prototype.

This demonstrates a getter/setter pair guarding a raw internal value (_balance), plus a static field that tracks state shared across every BankAccount instance rather than belonging to any single one. Expected result: $250.00 1 acct.balance = 250; looks like a plain assignment but actually runs the set balance(newValue) method, which validates the input.

describe() lives only on the base Shape class, yet it calls this.area() — because area() is overridden differently in Circle and Square, each instance's describe() automatically uses its own class's area() implementation. This is polymorphism: one shared method producing different results depending on the actual runtime type. Expected result: Circle has an area of 28.27. Square has an area of 16.

super("Circle") calls Shape's constructor to set this.name before Circle's own constructor logic runs.

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  area() {
    return this.width * this.height;
  }
}

const rect = new Rectangle(4, 5);
console.log(rect.area());
class BankAccount {
  static accountsCreated = 0;

  constructor(owner) {
    this.owner = owner;
    this._balance = 0;
    BankAccount.accountsCreated++;
  }

  get balance() {
    return `$${this._balance.toFixed(2)}`;
  }

  set balance(newValue) {
    if (newValue < 0) throw new Error("Balance cannot be negative.");
    this._balance = newValue;
  }
}

const acct = new BankAccount("Sam");
acct.balance = 250;
console.log(acct.balance);
console.log(BankAccount.accountsCreated);
class Shape {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return `${this.name} has an area of ${this.area()}.`;
  }
  area() {
    return 0;
  }
}

class Circle extends Shape {
  constructor(radius) {
    super("Circle");
    this.radius = radius;
  }
  area() {
    return Number((Math.PI * this.radius ** 2).toFixed(2));
  }
}

class Square extends Shape {
  constructor(side) {
    super("Square");
    this.side = side;
  }
  area() {
    return this.side ** 2;
  }
}

const shapes = [new Circle(3), new Square(4)];
for (const shape of shapes) {
  console.log(shape.describe());
}

How class syntax maps onto prototypes

class and extends are syntax on top of the exact same prototype chain from the previous lesson — they do not introduce a separate inheritance system.

class Dog extends Animal { ... }

   is equivalent, under the hood, to:

  Dog.prototype  --[[Prototype]]-->  Animal.prototype  --[[Prototype]]-->  Object.prototype  -->  null
       |                                    |
   (Dog's methods,                    (Animal's methods,
    e.g. bark())                       e.g. speak())

  new Dog(...) creates an instance whose own [[Prototype]] is Dog.prototype,
  so it can reach both Dog's methods and (via the chain) Animal's methods.

Important Rules

Methods defined in a class body live on the class's .prototype and are shared by all instances, not duplicated per instance.

A subclass constructor must call super(...) before it can use this.

static members belong to the class itself and are accessed via the class name, never via an instance.

Getters/setters are accessed like plain properties (no parentheses) even though they run code.

Where this shows up

Class syntax is the standard way to model structured data and behavior across the JavaScript ecosystem: React class components (in older codebases), Node.js error subclasses (class ValidationError extends Error), ORM model classes, and game object hierarchies (Player extends Character) all lean on constructor, extends, and super exactly as shown here.

class Dog extends Animal { constructor(name) { this.name = name; // used before super() super(); } } class Dog extends Animal { constructor(name) { super(); this.name = name; } }

In a subclass, this does not exist until super() has run (it initializes the inherited part of the instance) — using this before calling super() throws a ReferenceError.

class MathHelper { static square(x) { return x * x; } } const helper = new MathHelper(); helper.square(4); // TypeError

Static methods belong to the class itself, not to instances — calling helper.square(4) fails because square only exists on MathHelper directly, not on MathHelper.prototype.

[Multiple Choice] What must the first statement in a subclass's constructor be, if the constructor uses this? A. A call to super(...) B. A return statement C. A call to the static constructor D. There is no requirement

A. A call to super(...) — A subclass constructor must call super(...) — which runs the parent class's constructor and initializes the inherited part of the instance — before this can be accessed at all.

[True / False] Methods defined in a class body are copied individually onto every new instance created from that class. A. True B. False

B. False — False. Class methods are placed once on the class's .prototype object and shared by every instance via the prototype chain — they are not duplicated per instance, which is why they are memory-efficient.

Create a base class Employee with a constructor(name, baseSalary) and a method pay() that returns baseSalary. Create a subclass Manager extends Employee that adds a bonus in its constructor and overrides pay() to return baseSalary + bonus using super.pay().

class Employee { constructor(name, baseSalary) { this.name = name; this.baseSalary = baseSalary; } pay() { return this.baseSalary; } } class Manager extends Employee { constructor(name, baseSalary, bonus) { super(name, baseSalary); this.bonus = bonus; } pay() { return super.pay() + this.bonus; } } const mgr = new Manager("Priya", 60000, 5000); console.log(mgr.pay()); // 65000

Manager's pay() should call super.pay() to reuse the parent's logic, then add this.bonus to the result.