What Is a Class in Programming? JavaScript Guide
A class and an instance are not the same thing: the class supplies shared construction and behavior, while each instance is a separate object that can hold its own state.
A class in programming is a reusable definition for creating objects with related state and behavior, while each object created from that class is a separate instance with its own data.
What Is a Class in Programming?
Object-oriented programming organizes a program around objects that store state and perform related operations. A class defines how its instances are initialized and the shared operations they can perform.
Say a banking program needs hundreds of accounts. Every account has an owner and a balance, and every account can accept deposits and withdrawals. A class gathers those shared rules under one name instead of leaving each account as an unrelated collection of values and functions.
Five terms describe the parts:
- A class is the reusable definition, such as
BankAccount. - An object is a value that can contain data and behavior.
- An instance is an object created from a particular class.
- A property or field stores state, such as an owner or balance.
- A method is a function associated with the class, such as
deposit().
People often call a class a blueprint and an instance the building made from it. That comparison explains reuse, but it stops too soon in JavaScript. A JavaScript class is also a runtime value: you can assign it to a variable, pass it to a function, or return it from another function.
The class does not contain every account’s balance. It describes how an account receives a balance, while each instance keeps its own value.
That distinction drives the whole feature.
Build Your First JavaScript Class
A JavaScript class declaration starts with class, followed by its name and body. By convention, class names begin with a capital letter.
Start with a complete BankAccount class and two accounts:
class BankAccount {
static bankName = "Northstar Bank";
#balance;
constructor(owner, openingBalance = 0) {
this.owner = owner;
this.#balance = openingBalance;
}
deposit(amount) {
if (amount <= 0) {
throw new RangeError("Deposit must be greater than zero");
}
this.#balance += amount;
return this.#balance;
}
withdraw(amount) {
if (amount <= 0) {
throw new RangeError("Withdrawal must be greater than zero");
}
if (amount > this.#balance) {
throw new RangeError("Insufficient balance");
}
this.#balance -= amount;
return this.#balance;
}
getBalance() {
return this.#balance;
}
describe() {
return `${this.owner}: $${this.#balance}`;
}
}
const mayaAccount = new BankAccount("Maya", 100);
const rajAccount = new BankAccount("Raj", 250);
mayaAccount.deposit(50);
rajAccount.withdraw(20);
console.log(mayaAccount.describe());
console.log(rajAccount.describe());
console.log(mayaAccount.getBalance(), rajAccount.getBalance());
Maya: $150
Raj: $230
150 230
The constructor method runs when each account is created. Its owner and openingBalance arguments provide the starting state.
Inside the constructor, this refers to the new instance currently being initialized. During new BankAccount("Maya", 100), this is Maya’s account. During the next construction, it is Raj’s account.
this.owner is a public instance property. Code outside the class can read or change it. #balance is a private field, so only code inside the BankAccount class body can access it directly.
The two calls after construction prove that the instances change independently. Depositing 50 raises Maya’s balance from 100 to 150, while withdrawing 20 lowers Raj’s balance from 250 to 230. Neither operation touches the other account.
A class shares the rules, not the instance state.
The methods reject nonpositive amounts, and withdraw() also rejects amounts larger than the balance. They do not verify that an amount is a finite number, so production code should add a check such as Number.isFinite(amount). The methods also return the new balance, although this example prints the result through describe() and getBalance() instead.
Calling the class without new does not create an account. JavaScript throws a TypeError:
class Ticket {
constructor(number) {
this.number = number;
}
}
try {
Ticket(42);
} catch (error) {
console.log(error.name);
}
TypeError
This small standalone example uses a different class so it does not redeclare BankAccount. The rule is the same: JavaScript class instances are created with new.
Class basic syntax takes the declaration form further, while JavaScript Classes: How They Actually Work follows the prototype mechanics underneath it.
How a Class Creates and Organizes Objects
The new operator begins instance creation. For the BankAccount shown here, four results matter: JavaScript creates an object, connects that object to the class prototype, runs the constructor with this referring to the new object, and produces the initialized instance.
The members of BankAccount do not all live in the same place:
| Member | Where it belongs | What that means |
|---|---|---|
BankAccount | The class binding | Refers to the class value used with new |
owner | Each instance | Maya and Raj can hold different owners |
#balance | Each instance | Every account keeps separate private state |
deposit() | BankAccount.prototype | Instances share the ordinary method |
withdraw() | BankAccount.prototype | Instances share the ordinary method |
bankName | The class itself | Read as BankAccount.bankName |
Ordinary methods declared in the class body are installed on BankAccount.prototype. An instance finds them through its prototype connection, which is why both accounts can call deposit() without receiving separate method properties.
You can observe that arrangement directly:
class PrototypeAccount {
static bankName = "Northstar Bank";
deposit() {
return "deposit accepted";
}
}
const prototypeAccount = new PrototypeAccount();
console.log(prototypeAccount.hasOwnProperty("deposit"));
console.log(PrototypeAccount.prototype.hasOwnProperty("deposit"));
console.log(PrototypeAccount.bankName);
console.log(prototypeAccount.bankName);
false
true
Northstar Bank
undefined
deposit is not an own property of prototypeAccount; it is found on PrototypeAccount.prototype. bankName goes in the other direction: it belongs to the class, so PrototypeAccount.bankName works and prototypeAccount.bankName does not.
A static member is associated with the class rather than its instances. Static does not mean immutable. Code can still change a public static field unless the program applies some separate restriction.
Private fields follow another rule. A name beginning with # can only be used inside the body of the class that declares it. It is not an underscored public property with a naming convention, and even a derived class cannot access it directly.
For methods that depend on the current object, Object methods and this explains how the receiver determines the value of this.
Class vs. Object, Function, and Prototype
JavaScript has several ways to create values with related data and behavior. A class is one choice, not the default answer to every object-shaped problem.
This comparison names the four mechanisms directly:
| Mechanism | What you define | A practical fit |
|---|---|---|
| Class declaration | A construct used with new, with methods on its prototype | Many stateful instances sharing substantial behavior |
| Object literal | One object written directly with {} | One configuration, record, or namespace |
| Factory function | A function that returns an object | Combining existing behaviors or keeping data private through a closure |
| Constructor function and prototype | A function used with new, plus manually assigned prototype methods | Older APIs or direct study of prototype construction |
An object literal creates an object immediately:
const supportAccount = {
owner: "Lena",
balance: 75,
};
supportAccount is already the object. There is no separate reusable definition in this snippet.
A factory function creates and returns objects without class syntax:
function createAccount(owner, openingBalance) {
let balance = openingBalance;
return {
owner,
deposit(amount) {
balance += amount;
return balance;
},
getBalance() {
return balance;
},
};
}
const factoryAccount = createAccount("Lena", 75);
factoryAccount.deposit(25);
console.log(factoryAccount.getBalance());
100
Here, the local balance remains available to the returned methods through a closure. This provides privacy through function scope rather than a #balance class field. Factory Functions and the Factory Pattern develops that approach.
Before class syntax, constructor functions commonly paired new with manually assigned prototype methods:
function LegacyBankAccount(owner) {
this.owner = owner;
}
LegacyBankAccount.prototype.describe = function () {
return `Account owner: ${this.owner}`;
};
const legacyAccount = new LegacyBankAccount("Maya");
console.log(legacyAccount.describe());
Account owner: Maya
Class syntax still uses JavaScript’s prototype inheritance system. It does not replace prototypes, and describing it as merely shorter constructor-function syntax misses class-specific rules and semantics. Class bodies run in strict mode, a class cannot be called without new, and private elements have language-enforced access rules.
The practical difference remains concrete: a definition describes how instances are made, while an instance is one actual object carrying its own state.
Inheritance, Encapsulation, and Polymorphism
Three object-oriented terms describe different jobs.
Encapsulation keeps related state and behavior together while controlling how outside code reaches that state. BankAccount encapsulates its balance by keeping #balance private and exposing deposit(), withdraw(), and getBalance().
Inheritance creates a derived class from a parent class. In JavaScript, extends names one parent class, while languages make different choices here. Python, for example, supports multiple base classes.
Polymorphism lets code use the same operation with objects that provide different implementations. A derived class can override a parent method while callers continue to use the same method name.
Run this final program as a standalone replacement for the earlier BankAccount block:
class BankAccount {
static bankName = "Northstar Bank";
#balance;
constructor(owner, openingBalance = 0) {
this.owner = owner;
this.#balance = openingBalance;
}
deposit(amount) {
if (amount <= 0) {
throw new RangeError("Deposit must be greater than zero");
}
this.#balance += amount;
return this.#balance;
}
withdraw(amount) {
if (amount <= 0) {
throw new RangeError("Withdrawal must be greater than zero");
}
if (amount > this.#balance) {
throw new RangeError("Insufficient balance");
}
this.#balance -= amount;
return this.#balance;
}
getBalance() {
return this.#balance;
}
describe() {
return `${this.owner}: $${this.#balance}`;
}
}
class SavingsAccount extends BankAccount {
constructor(owner, openingBalance, interestRate) {
super(owner, openingBalance);
this.interestRate = interestRate;
}
describe() {
return `${super.describe()} at ${this.interestRate}%`;
}
}
const mayaAccount = new BankAccount("Maya", 150);
const rajAccount = new SavingsAccount("Raj", 230, 3);
for (const account of [mayaAccount, rajAccount]) {
console.log(account.describe());
}
Maya: $150
Raj: $230 at 3%
SavingsAccount extends BankAccount, so its instances receive the parent’s public behavior. Its constructor calls super() before using this, which initializes the parent part of the new object.
The derived describe() method overrides the parent method. It calls super.describe() to reuse the base description, then adds the interest rate.
The loop demonstrates polymorphism. Both values receive the same describe() call, but the savings account supplies its overridden result.
SavingsAccount still cannot read #balance directly. It must use public behavior inherited from BankAccount, such as getBalance(). Class inheritance covers derived classes and overriding in more detail.
Inheritance is optional. If the relationship between two types is weak, combining small functions or contained objects often produces a clearer design.
When Should You Use a Class?
Choose a class when the program has many objects that keep independent state and share a meaningful set of operations. Accounts, editor documents, game characters, and connection objects can fit that shape when their behavior is large enough to deserve a named abstraction.
A practical decision checklist keeps the choice grounded:
- Use a class when you need many stateful instances with the same substantial behavior.
- Use a class when callers already expect a recognizable API based on
new, methods, and possibly inheritance. - Use an object literal when you need one record, configuration object, or collection of related utilities.
- Use a factory function when you want to combine existing behaviors into each object or keep per-instance state private through a closure.
- Use separate functions when the data does not need to carry behavior with it.
Say a page needs one object containing display settings. An object literal names that record without adding construction machinery. Say it needs accounts with deposit rules, private balances, and several specialized account types. A class makes those shared rules visible in one place.
The payoff is a stable vocabulary for stateful objects. The price is another abstraction and, if inheritance enters the design, another relationship a reader must trace.
Use the class when that relationship clarifies the program. Leave it out when a plain object or function already tells the whole story.
JavaScript Fundamentals places classes alongside functions, objects, prototypes, and the other language features they depend on.