Skip to main content

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.

Key takeaway

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.

ProblemWithout classesWith classes
Many similar objectsCopy object literals or use factoriesOne named blueprint
Shared behaviorEasy to duplicate functionsMethods are shared through the prototype
InitializationScattered setup codeOne constructor path
Valid stateAnyone can mutate anythingMethods can protect invariants
Related utilitiesFloating helper functionsStatic methods on the class
Internal detailsPublic by defaultPrivate 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 ideaMeaningJavaScript class example
ObjectA value with data and behaviornew User("Ajay")
ClassA template for similar objectsclass User { ... }
InstanceOne object created from a classconst user = new User()
EncapsulationHide internals behind an API#balance, deposit()
InheritanceReuse behavior from another classclass Admin extends User
PolymorphismDifferent objects respond to the same methodshape.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:

BehaviorConstructor functionClass
Can be called without newYes, unless guardedNo, throws TypeError
Shared methodsManual Fn.prototype.method = ...Method syntax in class body
Private fieldsNo native equivalent#field
Class body strict modeDepends on surrounding codeAlways strict
Declaration hoistingFunction declarations are hoistedClass 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

QuestionAnswer
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/withdrawals
  • Cart: owns items and calculates totals
  • UserSession: owns auth state and expiration behavior
  • Color: owns internal color data and exposes conversion/read methods
  • ValidationError: carries error data and participates in instanceof checks

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
Classes are not automatically better

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 likeActuallyWhy
class creates a Java-style class systemJavaScript is still prototype-basedMethods are installed on .prototype
A class is just syntaxIt also has stricter semanticsClasses require new, run in strict mode, and support private elements
Methods belong to each objectMethods are sharedInstance methods live on the prototype
static means all instances share itStatic belongs to the class itselfCall User.method(), not user.method()
#private is like _private#private is language-enforcedOutside access is a syntax error

Resources