PLAINFINCH learn by building

Classes

You will write a class that groups data with the actions that use it, and create objects from it.

What problem does a class solve?

Some data comes with its own set of actions. A bakery order has a customer and a list of items, and you want to add an item, remove one and work out the total. You can keep the data in an object and write loose functions that take the order as an argument. That works, but the functions float around the file and nothing tells a reader which ones belong to orders.

A class bundles the data shape and the actions into one definition. You describe once what every order has and can do, then create as many orders as you need. Each order carries its own data, and every order shares the same methods.

Classes are common in browser APIs you already use. new Date(), new URL(...), new Map() and new FormData(form) all create objects from built-in classes. Writing your own uses the same pattern.

FigureA class called Order acts as a blueprint with a constructor and the methods add and total. Calling new Order twice makes two separate objects, one for Ada and one for Ben, each with its own items. Both objects share the methods from the class.A class called Order acts as a blueprint with a constructor and the methods add and total. Calling new Order twice makes two separate objects, one for Ada and one for Ben, each with its own items. Both objects share the methods from the class.
The class is a blueprint. Each new Order(...) runs the constructor once and gives back a separate object with its own data, while the methods are shared by all of them.

How do you write a class and create objects?

A class starts with class and a name in PascalCase, such as Order. Inside the braces you write methods: a name, parentheses for parameters and a body. There is no function keyword and no commas between methods.

The method called constructor is special. It runs once each time you create an object, and its job is to set up that object's starting data. Assignments like this.customer = customer; create properties on the new object.

Create an object, also called an instance, with new Order("Ada"). The new keyword makes an empty object, runs the constructor with this pointing at it, and returns it. Forgetting new throws a TypeError saying the class constructor cannot be invoked without it. Use the Step lens on the demo and watch order appear with its customer and items after the new line.

class Order {
  constructor(customer) {
    this.customer = customer;
    this.items = [];
  }

  add(name, price) {
    this.items.push({ name, price });
  }
}

const ada = new Order("Ada");
const ben = new Order("Ben");
ada.add("Rye loaf", 4.5);
console.log(ada.items.length, ben.items.length); // 1 0

What does this mean inside a method?

Inside a method, this is the object the method was called on. In ada.add("Rye loaf", 4.5), this is ada, so this.items is Ada's array. Call ben.add(...) and the same code works on Ben's array. That is how one method serves every instance.

Data on the object is always reached through this. Writing items.push(...) inside add looks for a variable called items in the surrounding scope, finds none and throws items is not defined. A bare name is sneakier, because browsers have a global window.name, so the code runs and shows an empty string.

The value of this depends on how the function is called. If you pass a method somewhere on its own, as in button.addEventListener("click", ada.add), the event system calls it with this set to the button. Wrap it in an arrow function so the method is still called on the object: button.addEventListener("click", () => ada.add("Bun", 2)).

const button = document.querySelector("#add-bun");

// Loses this: add runs with this === button
// button.addEventListener("click", ada.add);

button.addEventListener("click", () => {
  ada.add("Bun", 2);
});

What are getters for?

A getter is a method that you read like a property. Write get before the name: get count() { return this.items.length; }. Then order.count runs the method and gives the result, with no parentheses.

Getters suit values worked out from other data, like a count, a full name or a status label. They are always up to date, because they are computed each time you read them, and you never have to remember to update a stored copy. Keep them cheap and free of side effects; a reader expects reading a property to change nothing.

Calling a getter with parentheses is a common slip. order.count() first reads the number, then tries to call it, and throws order.count is not a function.

class Order {
  constructor(customer) {
    this.customer = customer;
    this.items = [];
  }

  get count() {
    return this.items.length;
  }
}

const order = new Order("Ada");
console.log(order.count); // 0

How do private fields keep data safe?

Any code can change a normal property: order.items = "oops" breaks every method that expects an array. A private field, written with # before its name, can only be read or changed by code inside the class body. It is declared at the top of the class, optionally with a starting value.

Code outside that tries order.#items gets a syntax error before anything runs, so the mistake shows up at once. Private fields have worked in every current browser for years, and they are real privacy: they do not appear in Object.keys or JSON.stringify, and the Console shows them only while debugging.

Use them for data that must stay consistent, such as a rented flag that only rent() and giveBack() should change, and offer a getter if outside code needs to read it.

class Bike {
  #rented = false;

  constructor(name) {
    this.name = name;
  }

  rent() {
    this.#rented = true;
  }

  get isRented() {
    return this.#rented;
  }
}

const tandem = new Bike("Blue tandem");
tandem.rent();
console.log(tandem.isRented); // true

What are static methods?

A static method belongs to the class itself, with no instance involved. You call it on the class name: Order.fromJSON(text). Inside it, this is the class, and there is no individual order to work on.

The common uses are helper constructors and utilities related to the class. Order.fromJSON can parse saved text and return a real Order with its methods, which a plain JSON.parse result does not have. Built-in examples include Array.from, Number.isInteger and Date.now.

class Order {
  constructor(customer) {
    this.customer = customer;
    this.items = [];
  }

  static fromJSON(text) {
    const data = JSON.parse(text);
    const order = new Order(data.customer);
    order.items = data.items;
    return order;
  }
}

const saved = Order.fromJSON('{"customer":"Ada","items":[]}');
console.log(saved instanceof Order); // true

How do extends and super work?

class CakeOrder extends Order makes a class that starts with everything Order has and adds or changes some of it. A CakeOrder has add and total, plus anything new, such as a message to pipe on top.

If the child class has its own constructor, it must call super(...) before using this. super(customer) runs the parent's constructor, which sets up customer and items. Forgetting it throws a ReferenceError about accessing this before calling super. Inside a method, super.total() calls the parent's version, which is useful when you want to add to it.

Inheritance chains get hard to follow quickly. One level, used for a clear "is a special kind of" relationship, is fine. When you find yourself three levels deep, plain functions or an object holding another object are usually easier to change.

class CakeOrder extends Order {
  constructor(customer, message) {
    super(customer);
    this.message = message;
  }

  total() {
    return super.total() + 3; // piping costs $3
  }
}

When is a plain object or function enough?

Classes are one tool among several, and beginners often reach for them too early, partly because AI tools and older tutorials produce them by default. If you have a single settings object, write an object literal. If you need to turn data into other data, such as formatting a price, write a function. Neither needs new, this or a constructor, and neither can lose this in a callback.

Data you save with JSON.stringify or receive from fetch is plain objects. Wrapping it in classes means converting it back and forth. In many small pages, an array of plain objects plus a few functions that take them as arguments is the simplest thing that works.

A class earns its place when you create many objects of the same kind, each with data that changes over time and rules about how it may change. A bike that can be rented and returned, a timer that can start and stop, or a game piece with a position fit well.

  • One object, no behaviour: an object literal.
  • A calculation or transformation: a function.
  • Many objects with changing state and rules: a class.
  • Data from JSON or an API: plain objects, unless you need methods on them.

How do you read and check class code?

In the Console, log an instance. Chrome and Firefox show its own properties first and the methods under [[Prototype]], which is where shared methods live. order instanceof Order tells you whether an object really came from the class, which helps when data was loaded from storage and lost its methods.

When an AI writes a class, check three things. Every property use inside a method has this.; callbacks that call methods use arrow functions; and the class is doing work that a plain function could not do just as well. Also look for methods written as arrow-function properties, like add = () => {...}. They fix this, but each object gets its own copy of the function, so use them only for methods you pass as callbacks.

Field notes

  • A class bundles data and the methods that work on that data.
  • The constructor sets up each new object you make with new.
  • Inside a method, this is the object the method was called on.
  • A plain object or function is often enough when you do not need many similar objects.

Easy to get wrong

Classes must be called with new. Without it, JavaScript throws a TypeError and no object is created.

Inside a method, the object's properties are reached through this. A bare items looks for an ordinary variable and throws.

Passing the method alone detaches it, so this becomes the button. The arrow function calls it on the order.

Methods in a class body are not separated by commas the way object properties are. The comma is a syntax error.

Questions people ask

Are JavaScript classes the same as in Java or Python?

The syntax looks similar, but underneath they use prototypes: methods live on a shared prototype object that every instance links to. For everyday use, you can treat them like classes in other languages.

Why does my object lose its methods after JSON.parse?

JSON stores data only, never functions or classes. Parsing gives a plain object. Rebuild the instance with new, often inside a static method like fromJSON.

Should I use # private fields or an underscore?

Use #. An underscore like _items is only a naming habit and anyone can still change it. #items is enforced by the language.

Can I add methods after creating the object?

You can assign functions to properties at any time, but it makes code hard to follow. Put methods in the class body so every instance has the same set.

Keep going