JavaScript Classes: When an Object Literal Is No Longer Enough
Write a class with a constructor, methods, a private field and a getter, and know when a plain object is still the better choice. After this, the rules about your data live in one place.
Course content
Sign in to save your progress.
-
Set Up Projects That Don't Break Each Other
-
Node.js and npm: Running JavaScript Outside the Browser
-
package.json and Lock Files: Same Install Every Time
-
-
-
Organize Code Other People Can Read
-
JavaScript Modules: import, export and Splitting One Big File
-
JavaScript Classes: When an Object Literal Is No Longer Enough
-
Destructuring and Spread Without the Confusion
-
-
-
Work With Real Data
-
Array Methods: map, filter, reduce and When a Loop Is Clearer
-
Working With JSON: parse, stringify and What Breaks
-
Dates in JavaScript Without Losing a Day
-
-
-
Waiting for Things
-
Callbacks to Promises: Why the Code Runs Out of Order
-
async/await and fetch: Calling a Real API
-
-
-
Prove It Works Before Someone Else Finds Out
-
try/catch and Custom Errors
-
Your First Tests With Vitest
-
-
- Final Check: JavaScript Intermediate
Skip ahead: If you already write classes with a constructor, methods and private fields, skip to "When not to use a class".
For one order, an object literal is enough: { id: 1042, items: [] }. The problem starts when you have fifty orders, and each one needs the same behaviour: add an item, compute the total, refuse a negative quantity. You either copy those functions around, or you put the data and the behaviour that belongs to it in one place. That place is a class.
From object literal to class
The literal version:
const order = {
id: 1042,
items: [],
};
order.items.push({ name: "Coffee", price: 180, qty: 2 });
order.items.push({ name: "Bread", price: 65, qty: -1 });
console.log(order.items.length);
2
Nothing stopped the quantity of -1. Any code anywhere can push anything into items. Now the class version:
class Order {
#items = [];
constructor(id) {
this.id = id;
}
addItem(name, price, qty) {
if (!Number.isInteger(qty) || qty < 1) {
throw new RangeError(`Quantity must be a whole number of at least 1, got ${qty}`);
}
this.#items.push({ name, price, qty });
}
get total() {
return this.#items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
get itemCount() {
return this.#items.length;
}
}
const order = new Order(1042);
order.addItem("Coffee", 180, 2);
order.addItem("Bread", 65, 1);
console.log(order.id, order.itemCount, order.total);
try {
order.addItem("Bread", 65, -1);
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
1042 2 425
RangeError: Quantity must be a whole number of at least 1, got -1
The parts
class Order { ... }is the blueprint.new Order(1042)makes one order from it, an instance.constructorruns once, whennewcreates the instance. Set up the starting data here.thisis the instance being worked on.this.idon one order is notthis.idon another.- Methods like
addItemare shared by every instance. Fifty orders, one copy of the function. #itemsis a private field. Only code inside the class can touch it. Outside,order.#itemsis a syntax error, not a quietundefined.get total()is a getter. You read it like a property,order.total, with no brackets, and it computes the value every time. The total can't get out of sync with the items because it isn't stored.
The getters and setters are covered in more detail in the tutorial JavaScript Classes With Getters and Setters.
⚠ Warning: Forgetting new is an error with classes, which is good: Order(1042) throws TypeError: Class constructor Order cannot be invoked without 'new'. The quieter mistake is losing this. If you pass a method as a callback, like button.addEventListener("click", order.addItem), this inside it is no longer the order. Wrap it: () => order.addItem("Coffee", 180, 1).
Inheritance, briefly
A class can extend another one and add or change behaviour:
class Order {
constructor(id) {
this.id = id;
}
describe() {
return `Order ${this.id}`;
}
}
class RushOrder extends Order {
constructor(id, deadline) {
super(id);
this.deadline = deadline;
}
describe() {
return `${super.describe()} (rush, due ${this.deadline})`;
}
}
console.log(new RushOrder(1043, "5 PM").describe());
Order 1043 (rush, due 5 PM)
super(id) runs the parent's constructor, and it has to come before you use this. Use extends when one thing really is a special kind of the other. Don't build towers of five classes deep. They're hard to follow, and a change at the bottom ripples up through everything.
When not to use a class
Classes aren't better by default. Use a plain object when:
- It's just data: a row from an API, a settings object, a JSON payload.
- There's no rule to protect.
- A couple of plain functions that take the data as an argument do the job.
Use a class when the data has rules that must always hold (quantity at least 1, status only moves forward), and when you'll create many of the same thing.
💡 Tip: If you send an instance through JSON.stringify, private fields and getters are left out. Only plain own properties are saved. Write a toJSON() method that returns exactly what you want sent.
Common mistakes
- Forgetting
this.inside a method and reading a variable that doesn't exist. - Passing a method as a callback and losing
this. - Storing a computed value (like a total) instead of using a getter, so it goes stale.
- Using
_itemsand trusting everyone to leave it alone. A real#itemsis enforced. - Creating a class for something that has no behaviour.
What happens if you skip this
You'll keep checking quantities in five different places, and one day the sixth place won't check. Then an order with a negative quantity reduces a customer's total, and you find out from the accounting report.
Pick one kind of data in your current project. What rule should always be true about it, and which line of code would enforce it today?
About
Write a class with a constructor, methods, a private field and a getter, and know when a plain object is still the better choice. After this, the rules about your data live in one place.
- Duration
- 5 min
- Last update
- 09/06/2026
Comments (0)
Sign in to commentNo comments yet.