Open the IRCTC app to book a train ticket and look at what happens on screen: every passenger gets a name, a berth, a fare, and a PNR status, but the rules for calculating that fare and formatting that ticket are identical for every single passenger. Fifty different people, one shared set of rules. That single observation — many objects, one blueprint — is the entire idea behind object-oriented programming, and ES6 classes are the tool JavaScript gives you to write it down cleanly. Before we touch the class keyword, though, we need to feel the actual pain it solves, because a tool only makes sense once you've suffered without it.
The Problem: Copy-Pasting Objects Doesn't Scale
Suppose you're building a small gradebook for your school's annual exam results. For one student, an object literal works fine:
const aarav = {
name: "Aarav",
marks: [88, 92, 79],
getAverage: function () {
let total = 0;
for (let m of this.marks) total += m;
return (total / this.marks.length).toFixed(2);
}
};
console.log(aarav.getAverage()); // "86.33"
Check that arithmetic before moving on, because tracing values is a habit you should build now: 88 + 92 + 79 = 259, and 259 divided by 3 is 86.333..., which toFixed(2) rounds and formats as the string "86.33". Good — the object works.
Now add a second student, Diya, with marks [95, 90, 100]. You copy the entire object literal, rename it, change the data — and paste the exact same ten-line getAverage function inside it again. A third student means a third copy. If your teacher later asks you to also compute the highest mark, you now have to find and edit that logic in every single copy, because each object has its own independent version of the function sitting inside it. This is not a small inconvenience — it is a structural problem. You have tangled two different things together that should be kept apart: the data that changes from student to student (name, marks) and the behaviour that stays identical for every student (how to compute an average). A class is JavaScript's way of separating those two things properly.
From Objects to Blueprints: Introducing the class Keyword
Here is the same gradebook rewritten with a class. Read it slowly — every line has a specific job.
class Student {
constructor(name, marks) {
this.name = name;
this.marks = marks;
}
getAverage() {
let total = 0;
for (let m of this.marks) total += m;
return (total / this.marks.length).toFixed(2);
}
}
const aarav = new Student("Aarav", [88, 92, 79]);
const diya = new Student("Diya", [95, 90, 100]);
console.log(aarav.getAverage()); // "86.33"
console.log(diya.getAverage()); // "95.00"
Verify Diya's number too, since a chapter that asks you to trust code without tracing it is teaching you the wrong habit: 95 + 90 + 100 = 285, and 285 / 3 = 95 exactly, so toFixed(2) gives "95.00".
Notice what changed structurally, not just visually. Student is not an object — it is a template for producing objects. It is defined exactly once, no matter how many students eventually pass through your gradebook. The constructor is a special method that runs automatically every time you build a new student, and its job is to attach the data that makes each student unique. The getAverage method describes behaviour that will be identical for every student who is ever created from this template. Data lives inside each object; behaviour lives once, inside the class.
What Actually Happens When You Write new
The word new is doing real, specific work, not just decorating your code — and CBSE exam questions love to test exactly this sequence, so trace it carefully. When JavaScript evaluates new Student("Aarav", [88, 92, 79]), four things happen in order:
- A brand-new, completely empty object is created in memory — think of it as a blank ID card with no fields filled in yet.
- That empty object is internally linked to
Student.prototype, which is where the class's methods actually live (more on this shortly). - The
constructorfunction runs, and inside it, the keywordthisrefers to that freshly created blank object. The linesthis.name = nameandthis.marks = markswrite onto the blank card. - The now-filled-in object is automatically returned — you never write
returnyourself inside a constructor for this to happen.
Only after all four steps finish does the variable aarav actually hold the finished object. This is why calling Student("Aarav", [88, 92, 79]) without new is not just bad style — it is a hard error. Class constructors in JavaScript refuse to run without new: you get TypeError: Class constructor Student cannot be invoked without 'new'. Regular functions are more forgiving about this; classes deliberately are not, precisely so that this four-step process can never be skipped by accident.
this — The Word That Changes Its Meaning
Students often try to memorize what this "equals," as if it were a fixed value. It isn't. Think of this as a on a form that a clerk fills in only when the form is actually being processed — the same blank constructor code produces a different filled-in card depending on which application is currently on the clerk's desk. Inside aarav's construction, this was Aarav's blank card. Inside diya's construction, the exact same three lines of constructor code ran again, but this time this pointed at Diya's blank card instead. The code is shared; what this refers to is not — it depends entirely on which object triggered the call.
Methods Aren't Duplicated: How Instances Share Behaviour
Here is a misconception that trips up almost everyone learning classes for the first time: the belief that every object created from a class carries its own private copy of each method, so that ten students in your gradebook means ten separate copies of getAverage sitting in memory. This is false, and you can prove it's false with one line of code:
console.log(aarav.getAverage === diya.getAverage); // true
That comparison returns true because it is literally the same function object being referenced by both. Only the data — name and marks — is stored separately on each instance, because the constructor writes those with this.name = ..., directly onto the new object. Methods declared in the class body, by contrast, are written once onto a shared object called Student.prototype, and every instance is silently linked to it. When you call aarav.getAverage(), JavaScript looks for getAverage directly on aarav first, doesn't find it there, then follows the link to Student.prototype, finds it there, and runs it with this set to aarav. This lookup chain is called the prototype chain, and it's the actual mechanism sitting underneath the friendlier class syntax — ES6 classes did not replace JavaScript's older prototype-based object system, they gave it a cleaner, more readable way to write the same thing.
The diagram below makes this concrete: one blueprint, three independent objects with their own data, but a single shared method that all three objects reach through the same link.
Static Methods: Behaviour That Belongs to the Class Itself
Sometimes a method logically belongs to the class as a whole rather than to any one object — a rule about students in general, not about one particular student. ES6 lets you mark such a method static:
class Student {
constructor(name, marks) {
this.name = name;
this.marks = marks;
}
getAverage() {
let total = 0;
for (let m of this.marks) total += m;
return (total / this.marks.length).toFixed(2);
}
static isPassingMark(mark) {
return mark >= 33;
}
}
console.log(Student.isPassingMark(40)); // true
console.log(Student.isPassingMark(20)); // false
console.log(aarav.isPassingMark(40)); // TypeError — not available on instances
A static method is called on the class name directly, never on an instance — Student.isPassingMark(40), not aarav.isPassingMark(40). That last line in the code above would throw an error, because static methods live on the class itself, not on Student.prototype where instance methods live, so instances have no link to reach them. Use static whenever a piece of logic doesn't need any particular object's data to run.
Getters: Computed Properties That Read Like Data
A get method lets you define a method that is called without parentheses, as though it were a plain property:
class Student {
constructor(name, marks) {
this.name = name;
this.marks = marks;
}
getAverage() {
let total = 0;
for (let m of this.marks) total += m;
return (total / this.marks.length).toFixed(2);
}
get resultLine() {
return `${this.name} scored an average of ${this.getAverage()}`;
}
}
const kabir = new Student("Kabir", [70, 65, 80]);
console.log(kabir.resultLine);
// "Kabir scored an average of 71.67"
Check that number: 70 + 65 + 80 = 215, and 215 / 3 = 71.666..., which rounds to "71.67". Notice kabir.resultLine is written with no parentheses at all — that's the entire point of a getter. It runs the method's code behind the scenes but presents the result the way a simple property would be read, which is useful whenever a value is really a computed summary of other data rather than something stored directly.
Inheritance: extends and super — Specialising a Blueprint
Now for the second big idea in object-oriented programming: some classes are more specific versions of other classes. Think about IRCTC ticket types again. Every ticket has a passenger name and a distance, and every ticket needs its fare calculated — but a Sleeper ticket adds a flat surcharge, while an AC 3-Tier ticket charges a multiplier plus a base amount and, optionally, a bedding charge. Rather than writing three unrelated classes and repeating the shared parts, you write one general Ticket class and let the specific ticket types extend it.
class Ticket {
constructor(passengerName, distanceKm) {
this.passengerName = passengerName;
this.distanceKm = distanceKm;
}
calculateFare() {
return this.distanceKm * 0.5; // base rate: ₹0.50 per km
}
printTicket() {
return `${this.passengerName}: ₹${this.calculateFare().toFixed(2)}`;
}
}
class SleeperTicket extends Ticket {
calculateFare() {
return super.calculateFare() + 20; // flat sleeper surcharge
}
}
class ACThreeTierTicket extends Ticket {
constructor(passengerName, distanceKm, hasBedding) {
super(passengerName, distanceKm);
this.hasBedding = hasBedding;
}
calculateFare() {
let fare = super.calculateFare() * 3 + 150; // AC3 multiplier + base charge
if (this.hasBedding) fare += 25;
return fare;
}
}
Let's trace three bookings, all for a 400 km journey, and verify every rupee:
const t1 = new Ticket("Meera", 400);
console.log(t1.printTicket()); // "Meera: ₹200.00"
const t2 = new SleeperTicket("Rohan", 400);
console.log(t2.printTicket()); // "Rohan: ₹220.00"
const t3 = new ACThreeTierTicket("Ishaan", 400, true);
console.log(t3.printTicket()); // "Ishaan: ₹775.00"
Trace each one by hand. t1: base fare is 400 * 0.5 = 200, so "Meera: ₹200.00". t2: SleeperTicket overrides calculateFare, calling super.calculateFare() to reuse the parent's base calculation (200) and adding the ₹20 surcharge, giving 220, so "Rohan: ₹220.00". t3: ACThreeTierTicket's constructor first calls super(passengerName, distanceKm), which runs Ticket's constructor to set up passengerName and distanceKm exactly as before — this call is mandatory; a subclass constructor that adds its own constructor method cannot use this until super() has run, because this doesn't exist yet until the parent has finished setting the object up. Then calculateFare runs: super.calculateFare() gives 200, multiplied by 3 is 600, plus the ₹150 base charge is 750, and since hasBedding is true, add ₹25 more for 775 — so "Ishaan: ₹775.00".
The most important detail to notice is that printTicket was written only once, inside Ticket, and never rewritten inside either subclass — yet it produced three completely different fares. That's because printTicket calls this.calculateFare(), and this is decided at the moment the method actually runs, not at the moment it was written. For t2, this is a SleeperTicket, so this.calculateFare() finds and runs SleeperTicket's overridden version, not Ticket's. This behaviour — one inherited method automatically adapting to whichever subclass calls it — is called polymorphism, and it's the entire practical payoff of inheritance: you write shared logic once, and each subclass only has to describe how it's different.
instanceof and a Second Misconception
A common wrong belief about extends is that it copies all of the parent class's code into the child class, the way copy-pasting a function into two files would. It does not. What extends actually does is link SleeperTicket.prototype to Ticket.prototype, extending the same prototype chain you saw earlier for shared methods — it does not duplicate a single line of code. You can confirm the link directly:
console.log(t2 instanceof SleeperTicket); // true
console.log(t2 instanceof Ticket); // true
console.log(Object.getPrototypeOf(SleeperTicket.prototype) === Ticket.prototype); // true
t2 is reported as an instance of both classes because the chain runs t2 → SleeperTicket.prototype → Ticket.prototype. When you call a method on t2, JavaScript checks t2 itself, then SleeperTicket.prototype, then — only if it isn't found there — Ticket.prototype. calculateFare is found at the second step because SleeperTicket overrides it; printTicket is found only at the third step, because SleeperTicket never redefines it. Nothing was ever duplicated; only links were created.
One more point worth being precise about, since accuracy matters more than sounding impressive: ES6, released in 2015, is what gave JavaScript the class keyword itself, along with constructor, instance methods, static methods, getters, and extends/super — everything used in this chapter. A feature you will often see written alongside classes in modern tutorials, private fields written with a # prefix like #balance, was standardized later, in ES2022, not in ES6 itself. Before that addition existed, and still very commonly today, JavaScript programmers signalled "please don't access this from outside the class" using an ordinary property name with a leading underscore, like this._balance — a convention, not an enforced rule, since JavaScript will happily let outside code read or change _balance if it wants to. Knowing which features actually belong to ES6 and which were added afterward isn't trivia — it explains why older browsers and older code you'll encounter can run classes perfectly but choke on a #-prefixed field.
Trace It Yourself
Work through these using the exact classes defined above — write out each step before checking your answer, the same way you traced the fare calculations.
- A new student, Priya, has marks
[60, 55, 50]. What string doesnew Student("Priya", [60, 55, 50]).getAverage()return? Show your division before rounding. - Using
Student.isPassingMark, is a mark of 33 a pass? What about 32.9? Explain why the comparison operator used in the method matters here. - A passenger books an
ACThreeTierTicketfor a 250 km trip withhasBeddingset tofalse. Compute the fare by hand, step by step, the wayt3was computed above. - Suppose
ACThreeTierTicketdid not callsuper(passengerName, distanceKm)inside its constructor before trying to usethis.hasBedding = hasBedding. What would JavaScript do, and why does the ordering rule exist? diya.getAverage === kabir.getAverage— predict the result before running it, and explain your reasoning using the concept of the prototype chain, not just "because it's true."
Summary
A class is a blueprint, not an object — it is written once and used to stamp out any number of independent objects. The constructor method runs automatically inside new, and this inside it refers to whichever blank object is currently being filled in, not to any single fixed thing. Data assigned with this.property = value lives separately on each instance, but methods declared in the class body live once on a shared object called the prototype, which every instance reaches through an internal link — proven by the fact that two instances' methods are the exact same function reference, not two copies of similar code. static methods belong to the class itself and are never available on instances. Getters let a method be read like a plain property, without parentheses. extends builds a specific class on top of a general one by linking prototypes together, not by copying code, and super() must run before this can be used in a subclass constructor because the parent constructor is what sets the object up in the first place. Because method lookup happens through this at call time, one inherited method can behave differently for every subclass that overrides part of its behaviour — the mechanism behind polymorphism. And finally: the class keyword, constructor, static methods, getters, and extends/super are all genuine ES6 (2015) features; private # fields arrived later, in ES2022, and the underscore-prefix convention you'll see in older code was always just a polite request, never an enforced boundary.
Practice Exercises
Now it is time to practice! Complete these challenges to solidify your understanding:
- Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
- Exercise 2: Find a real-world example where es6 classes: object-oriented programming in javascript is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
- Exercise 3: Create a mind-map connecting es6 classes: object-oriented programming in javascript to at least 3 other topics you have studied.