Skip to Content
JavaScript Intermediate: Write Code That Doesn't Fall Apart When the Data Changes
Intermediate
0 likes
Courses
JavaScript Intermediate: Write Code That Doesn't Fall Apart When the Data Changes

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.

Course Home

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.
  • constructor runs once, when new creates the instance. Set up the starting data here.
  • this is the instance being worked on. this.id on one order is not this.id on another.
  • Methods like addItem are shared by every instance. Fifty orders, one copy of the function.
  • #items is a private field. Only code inside the class can touch it. Outside, order.#items is a syntax error, not a quiet undefined.
  • 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 _items and trusting everyone to leave it alone. A real #items is 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
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.