Classes in JavaScript are syntactic sugar over prototypal inheritance. They don’t introduce a new inheritance model — they just give us a cleaner, more familiar syntax for doing what constructor functions and prototypes already did.
Basic Class Syntax
A class has a constructor method (called when we use new) and any number of methods:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
console.log(`Hi, I'm ${this.name}`);
}
getEmail() {
return this.email;
}
}
const user = new User("Manish", "manish@example.com");
user.greet(); // "Hi, I'm Manish"
Under the hood, greet and getEmail are added to User.prototype — exactly the same as the old User.prototype.greet = function() {} pattern.
Inheritance with extends and super
We use extends to create a child class and super to call the parent’s constructor or methods.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // MUST call super() before using this
this.breed = breed;
}
speak() {
console.log(`${this.name} barks`); // overrides parent method
}
info() {
super.speak(); // calls parent's speak()
console.log(`Breed: ${this.breed}`);
}
}
const dog = new Dog("Buddy", "Labrador");
dog.speak(); // "Buddy barks"
dog.info(); // "Buddy makes a sound" then "Breed: Labrador"
Important: if a child class has a constructor, it must call super() before accessing this.
Static Methods
Static methods belong to the class itself, not to instances. We call them on the class, not on objects.
class MathHelper {
static add(a, b) {
return a + b;
}
static random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
MathHelper.add(2, 3); // 5
MathHelper.random(1, 10); // random number between 1-10
// const m = new MathHelper();
// m.add(2, 3); // TypeError — add is not a method on instances
Use static methods for utility functions that don’t need instance data.
Private Fields (#field)
Private fields start with #. They can only be accessed from inside the class — not from outside, not even from subclasses.
class BankAccount {
#balance; // private field
constructor(owner, balance) {
this.owner = owner;
this.#balance = balance;
}
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount("Manish", 1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// console.log(account.#balance); // SyntaxError — private field
Getters and Setters
Getters and setters let us define computed properties that look like regular property access but run a function underneath.
class Circle {
constructor(radius) {
this.radius = radius;
}
get area() {
return Math.PI * this.radius ** 2;
}
get diameter() {
return this.radius * 2;
}
set diameter(value) {
this.radius = value / 2;
}
}
const c = new Circle(5);
console.log(c.area); // 78.54 — accessed like a property, no ()
console.log(c.diameter); // 10
c.diameter = 20; // calls the setter
console.log(c.radius); // 10
Classes are just prototypes underneath
This is important to understand — classes don’t add anything new to the language. They’re just a nicer way to write the same prototype-based code:
class User {
constructor(name) { this.name = name; }
greet() { console.log(`Hi, ${this.name}`); }
}
// is essentially the same as:
function User(name) { this.name = name; }
User.prototype.greet = function() { console.log(`Hi, ${this.name}`); };
// Proof:
console.log(typeof User); // "function" — classes are functions!
In simple language, classes give us a clean and familiar way to create objects with shared methods, set up inheritance, and organize our code. But under the hood, it’s still the same prototype chain we covered before. Think of classes as a nicer outfit on prototypes.