Using Classes
What is a class?
MDN: A class is a template for creating objects. In JavaScript, classes encapsulate data with code that works on that data, but they are still built on the prototype system.
In plain words: a class is a reusable blueprint for objects that should have the same shape and behavior.
The mental model
A class answers this question:
"I need many objects that behave the same way, but each object should keep its own data. Where should I put the shared behavior?"
The answer is: put the per-object data on the instance, and put the shared behavior on the class's prototype.
class User {
constructor(name) {
this.name = name; // each user gets its own name
}
login() {
return `${this.name} logged in`; // shared method
}
}
const ajay = new User("Ajay");
const ravi = new User("Ravi");
console.log(ajay.login()); // => "Ajay logged in"
console.log(ravi.login()); // => "Ravi logged in"
console.log(ajay.login === ravi.login); // => true
Why do we need classes?
Start without classes. Plain objects are enough when you have one thing:
const user = {
name: "Ajay",
login() {
return `${this.name} logged in`;
},
};
console.log(user.login()); // => "Ajay logged in"
The problem appears when the same kind of object keeps showing up.
const user1 = {
name: "Ajay",
login() {
return `${this.name} logged in`;
},
};
const user2 = {
name: "Ravi",
login() {
return `${this.name} logged in`;
},
};
console.log(user1.login()); // => "Ajay logged in"
console.log(user2.login()); // => "Ravi logged in"
This works, but the design is already leaking:
- the object shape is duplicated
- the method is duplicated
- future changes must be repeated everywhere
- there is no single place that says what a "user" means
So we usually introduce a factory function.
function createUser(name) {
return {
name,
login() {
return `${this.name} logged in`;
},
};
}
const user1 = createUser("Ajay");
const user2 = createUser("Ravi");
console.log(user1.login()); // => "Ajay logged in"
console.log(user2.login()); // => "Ravi logged in"
console.log(user1.login === user2.login); // => false
The factory removes repeated object creation code, but every call still creates a fresh login function. For two users this does not matter. For thousands of objects, repeated methods are noise in memory and a bad signal in design.
The important detail is where the method lives.
console.log(user1.login === user2.login); // => false
That false means each object owns a different function object:
user1
name: "Ajay"
login: Function A
user2
name: "Ravi"
login: Function B
A class puts the method in one shared place instead:
user1
name: "Ajay"
[[Prototype]] -> User.prototype
user2
name: "Ravi"
[[Prototype]] -> User.prototype
User.prototype
login: Function A
So classes are not "better than factories" in every case. A factory is great for simple object creation. A class helps when the objects need shared behavior, because prototype-backed method sharing is the default.
Classes are useful when object creation becomes a pattern, not a one-off. They give that pattern a name, one initialization path, and shared behavior.
The real problem classes solve
Classes do not exist because JavaScript cannot create objects without them. JavaScript had objects before classes.
Classes exist because object-heavy code needs organization.
| Problem | Without classes | With classes |
|---|---|---|
| Many similar objects | Copy object literals or use factories | One named blueprint |
| Shared behavior | Easy to duplicate functions | Methods are shared through the prototype |
| Initialization | Scattered setup code | One constructor path |
| Valid state | Anyone can mutate anything | Methods can protect invariants |
| Related utilities | Floating helper functions | Static methods on the class |
| Internal details | Public by default | Private fields can hide implementation |
Think of classes as a boundary:
class BankAccount {
#balance = 0;
deposit(amount) {
if (amount <= 0) {
throw new RangeError("Deposit must be positive");
}
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount();
account.deposit(100);
console.log(account.getBalance()); // => 100
// account.#balance = 999999; // => SyntaxError
The class is doing more than grouping functions. It protects a rule: the balance can only change through controlled behavior.
A useful class owns data plus the rules for safely changing that data.
Are classes part of OOP?
Yes. Classes are one way to write object-oriented programming.
OOP is not just "using the class keyword." It is a way to organize code around objects that combine state and behavior.
| OOP idea | Meaning | JavaScript class example |
|---|---|---|
| Object | A value with data and behavior | new User("Ajay") |
| Class | A template for similar objects | class User { ... } |
| Instance | One object created from a class | const user = new User() |
| Encapsulation | Hide internals behind an API | #balance, deposit() |
| Inheritance | Reuse behavior from another class | class Admin extends User |
| Polymorphism | Different objects respond to the same method | shape.area() |
But JavaScript has an important twist.
JavaScript classes are not a separate class engine
JavaScript is prototype-based. A class is cleaner syntax over that model.
class User {
constructor(name) {
this.name = name;
}
login() {
return `${this.name} logged in`;
}
}
console.log(typeof User); // => "function"
console.log(User.prototype.login); // => function login() { ... }
User is still a JavaScript value. Its instance methods are still installed on User.prototype.
How classes help in general
1. They name a domain concept
Without a class, "user" may be an informal object shape spread across files.
const user = {
id: 1,
name: "Ajay",
plan: "pro",
};
With a class, the concept becomes explicit.
class User {
constructor(id, name, plan) {
this.id = id;
this.name = name;
this.plan = plan;
}
canExportReports() {
return this.plan === "pro";
}
}
const user = new User(1, "Ajay", "pro");
console.log(user.canExportReports()); // => true
The benefit is not just syntax. Now the rule "pro users can export reports" lives beside the data it depends on.
2. They centralize initialization
Object setup often has defaults, validation, or derived values.
class CartItem {
constructor(productId, price, quantity = 1) {
if (quantity < 1) {
throw new RangeError("Quantity must be at least 1");
}
this.productId = productId;
this.price = price;
this.quantity = quantity;
}
getTotal() {
return this.price * this.quantity;
}
}
const item = new CartItem("book", 500, 2);
console.log(item.getTotal()); // => 1000
Every CartItem now goes through the same setup path.
3. They make shared behavior cheap
Class methods are shared by instances.
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count += 1;
}
}
const a = new Counter();
const b = new Counter();
console.log(a.increment === b.increment); // => true
The method is not recreated for every object. Each object keeps only its own count.
4. They give you an API boundary
If consumers reach into raw object properties, your internal representation becomes hard to change.
const color = {
values: [255, 0, 0],
};
console.log(color.values[0]); // => 255
Now outside code depends on values[0] meaning "red." If you later switch to another representation, outside code breaks.
class Color {
#rgb;
constructor(r, g, b) {
this.#rgb = [r, g, b];
}
get red() {
return this.#rgb[0];
}
}
const color = new Color(255, 0, 0);
console.log(color.red); // => 255
Consumers depend on color.red, not #rgb. That leaves the class free to change its internals later.
How this relates to JavaScript specifically
Before class, JavaScript already had constructor functions and prototypes.
function User(name) {
this.name = name;
}
User.prototype.login = function () {
return `${this.name} logged in`;
};
const user = new User("Ajay");
console.log(user.login()); // => "Ajay logged in"
The class version expresses the same core model with better syntax and stricter behavior.
class User {
constructor(name) {
this.name = name;
}
login() {
return `${this.name} logged in`;
}
}
const user = new User("Ajay");
console.log(user.login()); // => "Ajay logged in"
Important differences:
| Behavior | Constructor function | Class |
|---|---|---|
Can be called without new | Yes, unless guarded | No, throws TypeError |
| Shared methods | Manual Fn.prototype.method = ... | Method syntax in class body |
| Private fields | No native equivalent | #field |
| Class body strict mode | Depends on surrounding code | Always strict |
| Declaration hoisting | Function declarations are hoisted | Class declarations have TDZ behavior |
class User {}
User(); // => TypeError: Class constructor User cannot be invoked without 'new'
That error is a feature. It prevents accidental calls that were easy to make with constructor functions.
What, why, when
| Question | Answer |
|---|---|
| What is a class? | A template for creating similar objects with shared behavior. |
| Why use one? | To give repeated object creation one name, one setup path, and one behavior API. |
| When use one? | When an object owns state and exposes meaningful operations over that state. |
| When avoid one? | When you only need plain data, a one-off object, or stateless utility functions. |
Good class candidates:
BankAccount: owns balance and controls deposits/withdrawalsCart: owns items and calculates totalsUserSession: owns auth state and expiration behaviorColor: owns internal color data and exposes conversion/read methodsValidationError: carries error data and participates ininstanceofchecks
Weak class candidates:
- a plain API response object
- a group of unrelated helper functions
- a one-time config object
- a value that is easier to model as an immutable plain object
A class helps when state and behavior belong together. If you only have data, a plain object is clearer. If you only have behavior, a function is clearer.
The first gotchas
| Looks like | Actually | Why |
|---|---|---|
class creates a Java-style class system | JavaScript is still prototype-based | Methods are installed on .prototype |
| A class is just syntax | It also has stricter semantics | Classes require new, run in strict mode, and support private elements |
| Methods belong to each object | Methods are shared | Instance methods live on the prototype |
static means all instances share it | Static belongs to the class itself | Call User.method(), not user.method() |
#private is like _private | #private is language-enforced | Outside access is a syntax error |