You already know how to build an object by hand — a set of key-value pairs wrapped in curly braces. That works great for one object. But what happens when you need fifty objects that all share the same shape and the same behavior? A list of users, a fleet of products, an army of enemies in a game — each one with the same fields and the same set of things it can do. Writing each one out by hand, copy-pasting the methods every time, gets old fast and goes wrong easily.
A class is the fix. Think of it as a blueprint: you describe the shape and behavior once, and then you can stamp out as many objects from it as you want. Each object you create from a class is called an instance. Classes are JavaScript’s main tool for object-oriented programming, and once you’re comfortable with them, a whole category of code becomes much cleaner to write.
What a class actually is
Here’s the smallest useful class — one that models a person:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hi, I'm ${this.name} and I'm ${this.age}.`;
}
}
That’s the whole pattern in one block. Let’s break it down piece by piece, because every part earns its place.
The class keyword starts the declaration, followed by the name. By strong convention, class names use PascalCase — capitalized first letter, like Person, ShoppingCart, HttpClient. This isn’t enforced by the language, but everyone follows it, and it instantly signals “this is a class, not a regular variable or function.”
Inside the curly braces you define the body of the class: its constructor and its methods. Notice there are no commas between them — unlike an object literal, a class body separates its members with nothing but whitespace.
The constructor
The constructor is a special method that runs automatically every time you create a new instance. Its job is to set up the initial state — to give the fresh object its starting values.
You create an instance with the new keyword:
const alice = new Person("John Doe", 28);
console.log(alice.name); // "John Doe"
console.log(alice.greet()); // "Hi, I'm John Doe and I'm 28."
When you write new Person("John Doe", 28), JavaScript creates a brand-new empty object, runs the constructor with the arguments you passed, and hands you back the finished object. Inside the constructor, this refers to that new object. So this.name = name means “store the incoming name on this specific instance.”
The properties you set on this — name and age here — are sometimes called instance fields. Each instance gets its own copy, so two people can have different names without stepping on each other:
const a = new Person("John Doe", 28);
const b = new Person("Jane Doe", 34);
console.log(a.name); // "John Doe"
console.log(b.name); // "Jane Doe"
Forgetting new is a classic bug
If you call a class without new — Person("John Doe", 28) instead of new Person("John Doe", 28) — JavaScript throws a TypeError: Class constructor Person cannot be invoked without 'new'. That error message is actually doing you a favor; older constructor functions (before classes existed) would silently misbehave instead of warning you. Always pair a class with new.
Methods
Everything inside the class body that isn’t the constructor is a method — a function the instance can run. In the example above, greet() is a method. You call it on an instance with the familiar dot syntax: alice.greet().
Methods almost always work with this to reach the instance’s own data. That’s the whole point — a method is behavior that operates on the specific object it was called on:
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
describe() {
return `${this.width}×${this.height} = ${this.area()}`;
}
}
const r = new Rectangle(4, 5);
console.log(r.area()); // 20
console.log(r.describe()); // "4×5 = 20"
Notice that describe() calls this.area() — one method can call another on the same instance. The this. prefix is required; a bare area() would look for a standalone function in scope and fail.
Behind the scenes, methods live on the class’s prototype, not on each individual instance. That’s a memory win: a thousand Rectangle instances all share one copy of area() rather than each carrying its own. You don’t need to manage any of that — it just happens — but it’s worth knowing why methods belong in the class body rather than being assigned inside the constructor.
this depends on how a method is called
The value of this inside a method isn’t fixed to the instance — it depends on how the method is called. Calling obj.method() sets this to obj, but pulling the method out (const m = obj.method; m()) loses that connection and this becomes undefined in a class body (classes run in strict mode). This trips people up constantly when passing a method as a callback, such as to setTimeout or an event listener. If you’ve read the article on the this keyword, this is the same rule applied inside classes. The usual fixes are an arrow function wrapper (() => obj.method()) or binding the method.
Getters and setters
Sometimes you want a property that looks like a plain field from the outside but actually runs code when you read or write it. That’s what getters and setters are for. You define them with the get and set keywords:
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
get fahrenheit() {
return this.celsius * 9 / 5 + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) * 5 / 9;
}
}
const t = new Temperature(25);
console.log(t.fahrenheit); // 77 — read like a property, no ()
t.fahrenheit = 212; // write like a property
console.log(t.celsius); // 100
The magic here is that t.fahrenheit has no parentheses — you access it exactly like t.celsius, even though a function runs under the hood. The getter computes a value on demand, and the setter lets you assign through it and translate the value into stored state. This is great for derived or validated values: the outside world sees a simple property, while you keep control of what actually happens.
Static members
Everything so far has lived on instances. But sometimes a piece of data or behavior belongs to the class itself, not to any single instance. That’s a static member — marked with the static keyword, and called on the class name rather than on an instance:
class MathHelper {
static PI = 3.14159;
static double(n) {
return n * 2;
}
}
console.log(MathHelper.PI); // 3.14159
console.log(MathHelper.double(8)); // 16
Notice you never write new MathHelper() here — you call MathHelper.double(8) directly on the class. Static methods can’t use this to reach instance fields (there’s no instance), so they’re best for utility functions and factory helpers that relate to the class but don’t depend on a specific object. The built-in Math object works exactly this way: Math.max, Math.round, and friends are all static.
Inheritance with extends and super
Here’s where classes really start to pull their weight. Inheritance lets one class build on another, reusing its fields and methods while adding or changing what it needs. You use the extends keyword to say “this class is a more specific version of that one”:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // call the parent constructor
this.breed = breed;
}
speak() {
return `${this.name} barks!`;
}
}
const d = new Dog("Rex", "Beagle");
console.log(d.name); // "Rex" — inherited from Animal
console.log(d.breed); // "Beagle" — added by Dog
console.log(d.speak()); // "Rex barks!" — Dog's own version
A lot is happening, so let’s name the moving parts. Dog extends Animal means a Dog is an Animal and automatically gets everything Animal has. The super(name) call inside Dog’s constructor runs the parent’s constructor first — that’s what sets up this.name. And Dog defines its own speak(), which overrides the inherited one: when you call d.speak(), JavaScript finds Dog’s version and uses it instead of Animal’s.
You can also call the parent’s version explicitly from inside an override using super.methodName():
class Cat extends Animal {
speak() {
return super.speak() + " It meows.";
}
}
const c = new Cat("Milo");
console.log(c.speak()); // "Milo makes a sound. It meows."
Here super.speak() reaches up to Animal’s method, and then Cat adds to it. This “extend, don’t replace” approach is a common and clean way to build on existing behavior.
In a subclass, call super() before touching this
When a subclass has its own constructor, you must call super() before you use this. Try to read or write this.anything first and JavaScript throws a ReferenceError: Must call super constructor ... before accessing 'this'. The reason is concrete: the parent constructor is what actually creates the object that this points to, so until super() runs, there’s no this to work with yet. The fix is simple — make super(...) the first line of your subclass constructor.
Private fields
By default, every property on an instance is public — anyone with the object can read or change it. Often that’s fine. But sometimes you want internal state that the outside world can’t poke at directly. Modern JavaScript gives you private fields, marked with a # at the start of the name:
class BankAccount {
#balance = 0; // private — not reachable from outside
constructor(initial) {
this.#balance = initial;
}
deposit(amount) {
if (amount <= 0) return;
this.#balance += amount;
}
get balance() {
return this.#balance;
}
}
const account = new BankAccount(100);
account.deposit(50);
console.log(account.balance); // 150 — read through the getter
console.log(account.#balance); // SyntaxError — can't touch it from outside
The #balance field is only accessible from inside the class body. Code elsewhere can’t read it, can’t write it, and can’t even check whether it exists without erroring. This lets you protect your data: the only way to change the balance is through deposit(), which can enforce rules (no negative amounts). That guarantee — “the only path in is the one I designed” — is the heart of encapsulation.
The # is part of the name, forever
A private field’s # isn’t a modifier you can drop later — it’s literally part of the field’s identity. You always write this.#balance, never this.balance, to reach the private one. They’re also a genuinely different slot from a public property of a similar name: a class can technically have both #balance and balance, and they don’t collide. Private fields are widely supported in every modern browser and Node, so you can reach for them freely today.
Class expressions
Like functions, classes come in a declaration form (everything above) and an expression form, where you assign the class to a variable:
const Circle = class {
constructor(radius) {
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
};
const circle = new Circle(10);
console.log(circle.area().toFixed(2)); // "314.16"
You’ll mostly use the declaration form in everyday code, but class expressions are handy when you need to create a class conditionally, pass one into a function, or return one from a factory. Good to recognize, even if you don’t write it often.
When to use a class
Classes are a tool, not a mandate — JavaScript is happy to work with plain objects and functions too. Reach for a class when:
- You need many instances of the same shape and behavior (users, products, components, game entities).
- You have data and the behavior that operates on it that naturally belong together.
- You want inheritance — a hierarchy of related types that share a base.
- You want encapsulation — private state protected behind a controlled interface.
For a one-off bag of data with no behavior, a plain object literal is simpler and perfectly fine. Don’t wrap something in a class just because you can; do it when the blueprint-and-instances model genuinely fits what you’re building.
Classes are syntax over prototypes
It’s worth knowing the truth under the hood: JavaScript classes are syntactic sugar over the language’s older prototype system. A class doesn’t introduce a brand-new object model the way it does in some other languages — it’s a cleaner, more readable way to set up prototypes and constructor functions that already existed. You don’t need to master prototypes to use classes productively, but understanding that classes are prototypes in nicer clothing explains a lot of their quirks (like methods being shared, and this behaving the way it does). It’s a great topic to revisit once the basics feel comfortable.
Wrapping up
Classes give you a structured, reusable way to define objects and their behavior:
- A class is a blueprint; each object made from it with
newis an instance. - The
constructorruns automatically onnewand sets up the instance’s starting state viathis. - Methods are shared behavior that operate on the instance through
this. Getters/setters (get/set) look like properties but run code. staticmembers belong to the class itself and are called on the class name, not an instance.extendscreates a subclass that inherits from a parent;super()calls the parent constructor (always beforethis), andsuper.method()calls a parent method. A subclass can override inherited methods.- Private fields (
#name) hide internal state, giving you real encapsulation. - Under the hood, classes are friendly syntax over JavaScript’s prototypes.
With classes in your toolkit, you can model real-world things cleanly and scale from one object to thousands without repeating yourself. From here, the natural next step is pulling everything together with best practices — the habits that keep your JavaScript readable, predictable, and easy to maintain as your projects grow.