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); // trueAn 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); // 1050The 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.

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); // trueaisha.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); // trueThe 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.

Common class errors and how to repair them
Four failures reveal how the model works:
BankAccount('Neha', 1000)withoutnewthrows aTypeError. Usenew BankAccount('Neha', 1000).Reading
aisha.#balanceoutside the class is aSyntaxError. Readaisha.balanceinstead.Assigning
this.interestRate = 0.05beforesuper(...)in a derived constructor throws aReferenceError. Callsuperfirst.new BankAccount('Dev', 300)throws aRangeErrorbecause300 < 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); // 1100An 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); // trueUse 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
Constructor plus method: implement
Productsonew Product('Notebook', 250).priceAfterDiscount(12)returns220. Show250 × 12 / 100 = 30and250 - 30 = 220, reject percentages below0or above100, and leavepriceat250.Private state plus getter: implement
ScoreBoardwith private#scores,add(score), and getteraverage. Start with[72, 68], add80, reject105, and prevent direct access to the scores. The average is(72 + 68 + 80) / 3 = 220 / 3 = 73.333..., which must round to73.33.Inheritance: define
CarandElectricCar extends Carsonew ElectricCar('Tata', 'Nexon', 40).summary()returns exactly'Tata Nexon: 40 kWh'. Bothinstanceof ElectricCarandinstanceof Carmust betrue. Callsuper('Tata', 'Nexon')before assigning battery capacity40.
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.
newcreates an instance and calls its constructor.thisis chosen by the way a method is called.Private fields protect internal state.
Static members belong to the class.
extendsandsuperconnect 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.




