Learn
🤖 AI Hub 📰 News 📖 Dictionary 🧰 Resources
Grow
💼 Jobs 🚀 Community 🏆 Challenges
Explore by Stage
🌑 Zero 📚 Learn 🔧 Practice 🏗 Build 🚀 Launch 💰 Earn
Account
Login Sign Up
🏗️HTML 🎨CSS ⚡JavaScript 🐙Git & GitHub ⚛️React Soon 📘TypeScript Soon 🐍Python 🗄️Database & SQL Soon 🌐Web Development Soon ☁️Cloud & DevOps Soon 🤖AI & ML Soon 📄Resume Prep Soon 🎤Interview Prep Soon 💼Freelancing Soon 🚀SaaS & Side Projects Soon 🤖Automation Soon
Advanced 🕑 13 min read 👁 1 views 📅 Oct 21, 2026

Classes and Objects: Building a Bank Account in JavaScript

Build a bank account with JavaScript classes: constructors, methods, getters and setters, static members and private #balance fields, then extend it into a savings account that earns interest.

From Loose Objects to a Blueprint

Think about a bank. Every customer has an account with a holder name and a balance. Every account can take a deposit and give a withdrawal. The rules are the same for millions of accounts: you cannot withdraw more than you have, and nobody outside the bank can simply type a new balance.

In the JavaScript Objects lesson you made single objects with curly braces. That works for one account, but a bank needs a blueprint that creates as many accounts as it likes, each with its own data but the same behaviour. In JavaScript that blueprint is a class.

In this lesson you will build a BankAccount class step by step, protect its balance with private fields, and then create a SavingsAccount that earns interest by extending it. This style of programming, where data and the functions that work on it live together, is called object-oriented programming (OOP).

Your First Class

A class has a name (by convention starting with a capital letter), a constructor and some methods:

  • The constructor runs once, every time you create a new object. It sets up the starting data with this.
  • Methods are functions that every object made from the class can use.
  • The keyword new creates an object, called an instance, from the class.
class BankAccount {
  constructor(holder, openingBalance) {
    this.holder = holder;
    this.balance = openingBalance;
  }

  deposit(amount) {
    this.balance += amount;
    console.log(`${this.holder} deposited Rs ${amount}. Balance: Rs ${this.balance}`);
  }

  withdraw(amount) {
    if (amount > this.balance) {
      console.log(`Sorry ${this.holder}, not enough balance`);
      return;
    }
    this.balance -= amount;
    console.log(`${this.holder} withdrew Rs ${amount}. Balance: Rs ${this.balance}`);
  }
}

const priya = new BankAccount("Priya", 5000);
const arjun = new BankAccount("Arjun", 1200);

priya.deposit(2000);    // Priya deposited Rs 2000. Balance: Rs 7000
arjun.withdraw(1500);   // Sorry Arjun, not enough balance
arjun.withdraw(200);    // Arjun withdrew Rs 200. Balance: Rs 1000

console.log(priya.balance, arjun.balance);        // 7000 1000
console.log(priya.deposit === arjun.deposit);     // true (one shared method)

Inside a method, this means “the account this method was called on”. When you write priya.deposit(2000), this is priya. You will study this in depth in the JavaScript this Keyword lesson.

The last line shows something useful. Each account has its own holder and balance, but they all share one copy of each method. Behind the scenes, methods live on the class’s prototype, an object that every instance can look things up in. A class is a cleaner way to write this prototype system, which JavaScript has always had.

No commas between methods. Unlike an object literal, the methods inside a class body are not separated by commas. Adding one is a syntax error.

Getters and Setters

A getter is a method that you read like a property, without brackets. A setter is a method that runs when you assign a value to a property. They let you compute values on the fly and check data before saving it.

class BankAccount {
  constructor(holder, balance) {
    this.holder = holder;
    this.balance = balance;
    this._nominee = "Not added";
  }

  // A getter: read it like a property, no brackets
  get summary() {
    return `${this.holder} has Rs ${this.balance}. Nominee: ${this._nominee}`;
  }

  // A setter: runs when you assign a value
  set nominee(name) {
    if (name.trim().length < 2) {
      console.log("Nominee name is too short");
      return;
    }
    this._nominee = name.trim();
  }

  get nominee() {
    return this._nominee;
  }
}

const account = new BankAccount("Sunita", 8000);
console.log(account.summary);   // Sunita has Rs 8000. Nominee: Not added

account.nominee = "R";          // Nominee name is too short
account.nominee = "  Rohit  ";
console.log(account.nominee);   // Rohit
console.log(account.summary);   // Sunita has Rs 8000. Nominee: Rohit

The setter refuses a one-letter nominee name and trims extra spaces. The real value is stored in _nominee; the underscore is an old convention that means “please do not touch this from outside”. It is only a polite request. Next you will see how to make data truly private.

Static Properties and Methods

Some data belongs to the whole bank, not to one account: the bank’s name, or how many accounts it has opened. Mark these with static. You use them through the class name, like BankAccount.totalAccounts, not through an instance. Static methods are good for helper functions related to the class, such as formatting rupees.

class BankAccount {
  static bankName = "Janata Cooperative Bank";
  static totalAccounts = 0;

  constructor(holder, balance) {
    this.holder = holder;
    this.balance = balance;
    BankAccount.totalAccounts++;
    this.number = "JCB" + (1000 + BankAccount.totalAccounts);
  }

  static formatRupees(amount) {
    return amount.toLocaleString("en-IN", { style: "currency", currency: "INR" });
  }
}

const a = new BankAccount("Imran", 125000);
const b = new BankAccount("Divya", 4300.5);

console.log(BankAccount.bankName);                 // Janata Cooperative Bank
console.log(BankAccount.totalAccounts);            // 2
console.log(a.number, b.number);                   // JCB1001 JCB1002
console.log(BankAccount.formatRupees(a.balance));  // ₹1,25,000.00
console.log(a.formatRupees);                       // undefined (static belongs to the class)

Notice toLocaleString("en-IN", ...) groups the digits the Indian way, as 1,25,000, with the rupee symbol in front.

Private Fields with #

In our first class, anyone could write priya.balance = 99999999. A real bank would never allow that. A field whose name starts with #, like #balance, is private: only code written inside the class can read or change it. Outside code gets a syntax error if it even tries to mention it.

The usual pattern is a private field plus a public getter, so the outside world can see the balance but can only change it through methods like deposit that check the rules. This idea is called encapsulation: hide the data, expose safe actions.

class BankAccount {
  #balance = 0;   // private: only code inside this class can touch it

  constructor(holder, openingBalance) {
    this.holder = holder;
    this.#balance = openingBalance;
  }

  get balance() {
    return this.#balance;   // read-only from outside: there is no setter
  }

  deposit(amount) {
    if (amount <= 0) {
      throw new Error("Deposit must be more than zero");
    }
    this.#balance += amount;
  }
}

const account = new BankAccount("Kavya", 5000);

console.log(account.balance);   // 5000 (read through the getter)

account.deposit(1500);
console.log(account.balance);   // 6500

try {
  account.deposit(-500);
} catch (error) {
  console.log(error.message);   // Deposit must be more than zero
}

// console.log(account.#balance);  // a SyntaxError if you remove the // at the start

Because there is no setter, a line like account.balance = 1000000 cannot change the money. It is silently ignored in normal scripts and throws a TypeError in strict mode. Methods can be private too, such as #record() in the bigger example below. Use private members for anything that is an internal detail, so you can change it later without breaking code that uses your class.

Inheritance with extends and super

A savings account is a bank account with one extra feature: it earns interest. Instead of copying all the code, you write class SavingsAccount extends BankAccount. The new class, called the child or subclass, gets every method of the parent and can add or replace methods.

  • In the child’s constructor, call super(...) to run the parent’s constructor. You must do this before using this.
  • Inside a method, super.methodName() calls the parent’s version, so you can extend it instead of rewriting it.
  • instanceof checks whether an object was made from a class or any of its children.
class BankAccount {
  #balance;

  constructor(holder, openingBalance) {
    this.holder = holder;
    this.#balance = openingBalance;
  }

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

  deposit(amount) {
    this.#balance += amount;
  }

  describe() {
    return `${this.holder}: Rs ${this.#balance}`;
  }
}

class SavingsAccount extends BankAccount {
  constructor(holder, openingBalance, interestRate) {
    super(holder, openingBalance);   // run the parent constructor first
    this.interestRate = interestRate;
  }

  addInterest() {
    const interest = Math.round((this.balance * this.interestRate) / 100);
    this.deposit(interest);          // a method inherited from BankAccount
    return interest;
  }

  describe() {
    return super.describe() + ` (savings at ${this.interestRate}% a year)`;
  }
}

const ramesh = new SavingsAccount("Ramesh", 20000, 3.5);
console.log(ramesh.addInterest());   // 700
console.log(ramesh.describe());      // Ramesh: Rs 20700 (savings at 3.5% a year)

console.log(ramesh instanceof SavingsAccount);   // true
console.log(ramesh instanceof BankAccount);      // true
console.log(new BankAccount("Asha", 100) instanceof SavingsAccount);   // false

One detail: the child cannot read the parent’s #balance directly. Private really means private, even to children. The child uses the public balance getter and the deposit method instead, which is exactly what keeps the rules in one place.

Real-Life Example: A Savings Account Page

Here is a small banking page. The BankAccount class keeps a private balance and a private statement history. The SavingsAccount child adds interest and replaces canWithdraw to enforce a minimum balance of Rs 1,000. Notice how the page code never touches the balance directly; it only calls methods and shows errors.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Savings Account</title>
  <style>
    body { font-family: sans-serif; padding: 16px; max-width: 420px; }
    .balance { font-size: 28px; font-weight: bold; }
    input, button { padding: 8px; font-size: 16px; margin: 4px 0; }
    li { margin: 4px 0; }
    .error { color: crimson; }
  </style>
</head>
<body>
  <h2 id="title"></h2>
  <p class="balance" id="balance"></p>
  <input id="amount" type="number" min="1" placeholder="Amount in Rs">
  <div>
    <button id="deposit">Deposit</button>
    <button id="withdraw">Withdraw</button>
    <button id="interest">Add yearly interest</button>
  </div>
  <p id="message"></p>
  <h3>Statement</h3>
  <ul id="statement"></ul>

  <script>
    class BankAccount {
      #balance;
      #history = [];

      constructor(holder, openingBalance) {
        this.holder = holder;
        this.#balance = openingBalance;
        this.#record("Opening balance", openingBalance);
      }

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

      get history() {
        return [...this.#history];   // a copy, so outside code cannot change it
      }

      canWithdraw(amount) {
        return amount <= this.#balance;
      }

      deposit(amount, note = "Deposit") {
        if (!(amount > 0)) throw new Error("Enter an amount more than zero");
        this.#balance += amount;
        this.#record(note, amount);
      }

      withdraw(amount) {
        if (!(amount > 0)) throw new Error("Enter an amount more than zero");
        if (!this.canWithdraw(amount)) throw new Error("This withdrawal is not allowed");
        this.#balance -= amount;
        this.#record("Withdrawal", -amount);
      }

      #record(note, amount) {
        this.#history.push({ note, amount, balance: this.#balance });
      }

      static format(amount) {
        return amount.toLocaleString("en-IN", { style: "currency", currency: "INR" });
      }
    }

    class SavingsAccount extends BankAccount {
      static MIN_BALANCE = 1000;

      constructor(holder, openingBalance, interestRate) {
        super(holder, openingBalance);
        this.interestRate = interestRate;
      }

      canWithdraw(amount) {
        return this.balance - amount >= SavingsAccount.MIN_BALANCE;
      }

      addInterest() {
        const interest = Math.round((this.balance * this.interestRate) / 100);
        this.deposit(interest, "Interest at " + this.interestRate + "%");
      }
    }

    const account = new SavingsAccount("Neha Gupta", 15000, 4);
    const message = document.querySelector("#message");
    const amountInput = document.querySelector("#amount");

    function render() {
      document.querySelector("#title").textContent = account.holder + ", Savings Account";
      document.querySelector("#balance").textContent = BankAccount.format(account.balance);
      const list = document.querySelector("#statement");
      list.innerHTML = "";
      account.history.forEach((entry) => {
        const li = document.createElement("li");
        li.textContent = entry.note + ": " + BankAccount.format(entry.amount) +
          " (balance " + BankAccount.format(entry.balance) + ")";
        list.append(li);
      });
    }

    function run(action) {
      try {
        action(Number(amountInput.value));
        message.className = "";
        message.textContent = "Done.";
      } catch (error) {
        message.className = "error";
        message.textContent = error.message;
      }
      render();
    }

    document.querySelector("#deposit").addEventListener("click", () => run((n) => account.deposit(n)));
    document.querySelector("#withdraw").addEventListener("click", () => run((n) => account.withdraw(n)));
    document.querySelector("#interest").addEventListener("click", () => run(() => account.addInterest()));

    render();
  </script>
</body>
</html>

Try these changes:

  1. Change MIN_BALANCE to 5000 and try to withdraw Rs 12,000.
  2. Add a getter totalDeposits to BankAccount that adds up all positive amounts in the history, and show it on the page.
  3. Add a static method BankAccount.compare(a, b) that returns the holder with the higher balance.

When Classes Help, and When Plain Objects Are Enough

Use a class whenUse a plain object when
You need many objects with the same behaviour, like 1,000 accountsYou have one simple thing, like a settings object
Data must be protected by rules (private fields and methods)It is just data, such as JSON from an API
Types are related and share code (savings and current accounts)Small helper functions are enough

Do not force everything into classes. Much modern JavaScript uses plain objects and functions, and that is fine. Reach for a class when objects have both state and rules that belong together.

Common Mistakes

  • Forgetting new. Calling a class like a function throws an error:
class BankAccount {
  constructor(holder) {
    this.holder = holder;
  }
}

const good = new BankAccount("Ravi");
console.log(good.holder);   // Ravi

const bad = BankAccount("Ravi");   // ERROR: TypeError: Class constructor BankAccount cannot be invoked without 'new' 
  • Using this before super() in a child constructor. This throws a ReferenceError. Call super(...) as the first line.
  • Putting commas between methods, as you would in an object literal. Class bodies have no commas.
  • Calling a static method on an instance, like account.formatRupees(). Static methods belong to the class: BankAccount.formatRupees().
  • Passing a method as a callback, like button.addEventListener("click", account.deposit). The method loses its this. Use an arrow function, () => account.deposit(100), as the bank page does.

Practice Task: A Current Account with Overdraft

Shops often use a current account that may go below zero, up to an agreed overdraft limit. Starting from the BankAccount class below, create a CurrentAccount child class that:

  • Takes an extra overdraftLimit in its constructor and calls super.
  • Overrides canWithdraw so the balance may go down to minus the limit.
  • Has a getter isOverdrawn that is true when the balance is below zero.
  • Passes the tests written in the comments.
class BankAccount {
  #balance;

  constructor(holder, openingBalance) {
    this.holder = holder;
    this.#balance = openingBalance;
  }

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

  canWithdraw(amount) {
    return amount <= this.#balance;
  }

  withdraw(amount) {
    if (!this.canWithdraw(amount)) {
      throw new Error(`${this.holder}: withdrawal of Rs ${amount} refused`);
    }
    this.#balance -= amount;
  }
}

// TODO 1: create class CurrentAccount that extends BankAccount.
//   The constructor takes (holder, openingBalance, overdraftLimit).
// TODO 2: override canWithdraw so the balance may go down to -overdraftLimit.
// TODO 3: add a getter isOverdrawn that is true when the balance is below 0.
// TODO 4: test it:
//   const shop = new CurrentAccount("Sharma Kirana Store", 3000, 5000);
//   shop.withdraw(7000);  -> balance -4000, isOverdrawn true
//   shop.withdraw(2000);  -> refused (would go to -6000)
//   console.log(shop instanceof BankAccount);  -> true
Show one possible answer (try it yourself first!)
class BankAccount {
  #balance;

  constructor(holder, openingBalance) {
    this.holder = holder;
    this.#balance = openingBalance;
  }

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

  canWithdraw(amount) {
    return amount <= this.#balance;
  }

  withdraw(amount) {
    if (!this.canWithdraw(amount)) {
      throw new Error(`${this.holder}: withdrawal of Rs ${amount} refused`);
    }
    this.#balance -= amount;
  }
}

class CurrentAccount extends BankAccount {
  constructor(holder, openingBalance, overdraftLimit) {
    super(holder, openingBalance);
    this.overdraftLimit = overdraftLimit;
  }

  canWithdraw(amount) {
    return this.balance - amount >= -this.overdraftLimit;
  }

  get isOverdrawn() {
    return this.balance < 0;
  }
}

const shop = new CurrentAccount("Sharma Kirana Store", 3000, 5000);

shop.withdraw(7000);
console.log(shop.balance, shop.isOverdrawn);   // -4000 true

try {
  shop.withdraw(2000);
} catch (error) {
  console.log(error.message);   // Sharma Kirana Store: withdrawal of Rs 2000 refused
}

console.log(shop instanceof BankAccount);      // true

Key Takeaways

  • A class is a blueprint. new ClassName(...) runs the constructor and returns a new instance.
  • Methods are shared by all instances. Getters and setters look like properties but run code.
  • static members belong to the class itself, not to each object.
  • Fields and methods starting with # are truly private. Expose safe methods and getters instead.
  • extends creates a child class; super() calls the parent constructor and super.method() the parent method.
  • instanceof checks whether an object comes from a class or its children.

❓ Frequently Asked Questions

They look similar, but under the hood JavaScript classes are built on prototypes. A class is a cleaner way to write prototype-based code, which is why typeof a class gives "function".
#balance is truly private: the language blocks any access from outside the class. _balance is a normal public property, and the underscore is only a naming convention that asks other programmers not to use it.
No. A JavaScript class can extend only one parent. If you need to share behaviour between unrelated classes, use helper functions or put shared objects inside the class as fields.
No. If you leave it out, JavaScript adds an empty one. In a child class, the default constructor simply passes all its arguments to super.
Was this lesson helpful?

🎯 Finished this lesson?

🔗 Related Tutorials
🤖 Got a Doubt? Ask AI
Get instant answers in Hindi or English
🤖
Z2T Bot
Online
Ask me anything about this tutorial — I'm here! 👋