Classes in JavaScript: A Practical Tutorial with Runnable Examples

Learn what class syntax adds to JavaScript by building and extending a BankAccount. Trace exact values, repair common mistakes, and finish with three exercises.

KnowledgeGate Team

Exam prep & CS education

Updated 29 Aug 20265 min read

You can already write objects and functions, but class, constructor, new, this, extends, and super may still feel like overlapping ideas. JavaScript class syntax looks familiar, but instances still use the language's prototype-based object model. Start with a BankAccount, trace construction, private state, inheritance, and exact outputs, then repair common errors and solve three exercises. Run the code in a modern browser console or current Node.js environment.

What a JavaScript class creates

A class is a reusable definition for objects with initial state and shared behaviour. class names the definition, constructor initialises each instance, new creates that instance and calls the constructor, and this refers to the instance receiving a method call. Ordinary methods live on the class prototype, so JavaScript does not copy a separate function onto every instance.

class Badge {
  constructor(label) {
    this.label = label;
  }

  describe() {
    return `Badge: ${this.label}`;
  }
}

const learnerBadge = new Badge('JavaScript');
console.log(learnerBadge.label === 'JavaScript');       // true
console.log(learnerBadge.describe() === 'Badge: JavaScript'); // true
console.log(learnerBadge instanceof Badge);             // true

An object literal creates one object directly. A class gives you a repeatable construction process and shared methods. If you are comparing this model with Java, Python, or C++, use Programming Languages as the broader path, but do not assume their class systems are identical.

Build a complete BankAccount class and trace every value

This class keeps its balance private, exposes a getter, and validates construction and transactions.

class BankAccount {
  #balance;
  static minimumOpeningBalance = 500;

  constructor(owner, openingBalance) {
    if (openingBalance < BankAccount.minimumOpeningBalance) {
      throw new RangeError('Opening balance must be at least 500');
    }
    this.owner = owner;
    this.#balance = openingBalance;
  }

  deposit(amount) {
    if (amount <= 0) throw new RangeError('Deposit must be positive');
    this.#balance += amount;
    return this.#balance;
  }

  withdraw(amount) {
    if (amount <= 0) throw new RangeError('Withdrawal must be positive');
    if (amount > this.#balance) return false;
    this.#balance -= amount;
    return true;
  }

  get balance() {
    return this.#balance;
  }
}

const aisha = new BankAccount('Aisha', 1200);
console.log(aisha.owner, aisha.balance); // Aisha 1200
console.log(aisha.deposit(300));         // 1500
console.log(aisha.withdraw(450));        // true
console.log(aisha.balance);              // 1050
console.log(aisha.withdraw(2000));       // false
console.log(aisha.balance);              // 1050

The successful path is 1200 + 300 - 450 = 1050. The attempted withdrawal of 2000 returns false, so it performs no subtraction and the balance stays 1050. These validation rules and calculations are a teaching model, not a design for real banking software.

Diagram of the BankAccount class and its aisha instance, tracing deposit and withdrawal steps to a final balance of 1050.

Instance state, private fields, getters, and static members

Each instance owns separate state even though the methods are shared:

const kabir = new BankAccount('Kabir', 800);
console.log(kabir.deposit(50));            // 850
console.log(kabir.balance === 850);        // true
console.log(aisha.balance === 1050);       // true
console.log(aisha.deposit === kabir.deposit); // true

aisha.balance legitimately reads 1050 through the getter. Writing aisha.#balance outside the class is a parse-time SyntaxError; the getter does not make the private field writable. A name such as _balance would only signal privacy by convention, while #balance is enforced by JavaScript.

The static value belongs to the class: BankAccount.minimumOpeningBalance === 500, but aisha.minimumOpeningBalance === undefined. Static members describe class-level data or operations, not per-instance state.

Extend the example with inheritance and super

extends connects a child class to a parent. In a derived constructor, super(...) must run before you use this because it performs the parent construction.

class SavingsAccount extends BankAccount {
  constructor(owner, openingBalance, interestRate) {
    super(owner, openingBalance);
    this.interestRate = interestRate;
  }

  addYearlyInterest() {
    const interest = this.balance * this.interestRate;
    this.deposit(interest);
    return interest;
  }
}

const ria = new SavingsAccount('Ria', 2000, 0.05);
console.log(ria.addYearlyInterest());       // 100
console.log(ria.balance);                   // 2100
console.log(ria instanceof SavingsAccount); // true
console.log(ria instanceof BankAccount);    // true

The arithmetic is 2000 × 0.05 = 100, then 2000 + 100 = 2100. This simple annual calculation is teaching arithmetic, not a financial product rule. ria owns its state, addYearlyInterest is found on SavingsAccount.prototype, and deposit plus the balance getter are found through BankAccount.prototype.

Prototype-chain diagram for the ria SavingsAccount, where addYearlyInterest adds 100 and raises the balance to 2100.

Common class errors and how to repair them

Four failures reveal how the model works:

  • BankAccount('Neha', 1000) without new throws a TypeError. Use new BankAccount('Neha', 1000).

  • Reading aisha.#balance outside the class is a SyntaxError. Read aisha.balance instead.

  • Assigning this.interestRate = 0.05 before super(...) in a derived constructor throws a ReferenceError. Call super first.

  • new BankAccount('Dev', 300) throws a RangeError because 300 < 500. The object is not constructed, which is why this guard protects initial state in the constructor.

There is also a common this trap:

const looseDeposit = aisha.deposit;
try {
  looseDeposit(50);
} catch (error) {
  console.log(error instanceof TypeError); // true
}

const boundDeposit = aisha.deposit.bind(aisha);
console.log(boundDeposit(50)); // 1100
console.log(aisha.balance);     // 1100

An unbound call has no receiving BankAccount instance. bind(aisha) supplies that receiver, so the deposit changes Aisha's balance from 1050 to 1100. This is a targeted repair, not a reason to convert every method into an arrow-field method.

How code-tracing questions and technical screens test classes

Class questions can ask you to predict instance state, distinguish static and instance members, identify a missing new or super, follow inherited behaviour, or implement a small written contract. Avoid guessing. Trace the objects.

class Counter {
  count = 0;
  increment() { return ++this.count; }
}

const c1 = new Counter();
const c2 = new Counter();
console.log([c1.increment(), c1.increment(), c2.increment()]); // [1, 2, 1]
console.log(c1.count, c2.count);                                // 2 1
console.log(c1.increment === c2.increment);                     // true

Use four steps: mark each own field, locate every method or getter, bind this to the object left of the call, then update values in call order. Here c1 and c2 have separate count fields but share the prototype method.

Three exercises with exact acceptance checks

  1. Constructor plus method: implement Product so new Product('Notebook', 250).priceAfterDiscount(12) returns 220. Show 250 × 12 / 100 = 30 and 250 - 30 = 220, reject percentages below 0 or above 100, and leave price at 250.

  2. Private state plus getter: implement ScoreBoard with private #scores, add(score), and getter average. Start with [72, 68], add 80, reject 105, and prevent direct access to the scores. The average is (72 + 68 + 80) / 3 = 220 / 3 = 73.333..., which must round to 73.33.

  3. Inheritance: define Car and ElectricCar extends Car so new ElectricCar('Tata', 'Nexon', 40).summary() returns exactly 'Tata Nexon: 40 kWh'. Both instanceof ElectricCar and instanceof Car must be true. Call super('Tata', 'Nexon') before assigning battery capacity 40.

JavaScript classes: the short version and next step

Keep this six-line model beside your next practice session:

  • A class defines construction and shared behaviour.

  • new creates an instance and calls its constructor.

  • this is chosen by the way a method is called.

  • Private fields protect internal state.

  • Static members belong to the class.

  • extends and super connect parent and child behaviour.

Rerun the Aisha, Kabir, and Ria traces without looking at their outputs. For structured practice, continue with the Complete JavaScript Course. The MERN Stack Course: Full Stack Development is the broader application path, while Coding & Skill Development Courses is the neutral catalogue hub. Use Web Development when you want the wider browser and server topic map.