Emmanuel Akinfulubi

Lesson 07 of 1512 min read

class: the Same Thing, Less Typing

Now the syntax everyone actually writes. You already know what it does, because you built it by hand over the last three lessons.

By the end: Write a class with a constructor, fields and methods.

Everything you built by hand in lessons 04 to 06 has a shorter spelling. Here it is.

The same bus, as a class

class Bus {
  constructor(plate, seatsLeft) {
    this.plate = plate;
    this.seatsLeft = seatsLeft;
  }

  describe() {
    return this.plate + ' has ' + this.seatsLeft + ' seats left';
  }

  book(n) {
    this.seatsLeft -= n;
    return this;
  }
}

const b = new Bus('LAG-441-XA', 9);
console.log(b.describe());
console.log(b.book(3).describe());

Output


      

Now the proof that this is the constructor-plus-prototype pattern with nicer syntax and nothing more.

A class is a function, and its methods are on the prototype

class Bus {
  constructor(plate) { this.plate = plate; }
  describe() { return this.plate; }
}

console.log('typeof Bus:', typeof Bus);

const a = new Bus('A');
const b = new Bus('B');

console.log('methods are shared:', a.describe === b.describe);
console.log('describe lives on the prototype:',
  Object.hasOwn(Bus.prototype, 'describe'));
console.log('the instance owns only data:', Object.keys(a));
console.log('chain:', Object.getPrototypeOf(a) === Bus.prototype);

Output


      

The parts of a class body

Everything that can appear inside a class

class Example {
  // 1. A public field, with a default. Assigned to the instance.
  seats = 14;

  // 2. A private field. The # is part of the name. Lesson 08.
  #serial = 0;

  // 3. A static field: belongs to the class itself. Lesson 10.
  static built = 0;

  // 4. The constructor. Runs on `new`. At most one per class.
  constructor(plate) {
    this.plate = plate;
    Example.built++;
  }

  // 5. A method. Lands on Example.prototype.
  describe() { return this.plate; }

  // 6. A getter, read like a property. Lesson 09.
  get summary() { return this.plate + '/' + this.seats; }

  // 7. A setter, written like a property.
  set summary(value) { [this.plate, this.seats] = value.split('/'); }

  // 8. A static method: called on the class, not on an instance.
  static fromLabel(label) { return new Example(label.trim()); }

  // 9. An arrow field: bound to this instance forever. Lesson 03.
  report = () => this.plate + ' reporting';
}

All nine, working

class Example {
  seats = 14;
  #serial = Math.floor(Math.random() * 900 + 100);
  static built = 0;

  constructor(plate) {
    this.plate = plate;
    Example.built++;
  }

  describe() { return `${this.plate} (${this.seats})`; }
  get summary() { return `${this.plate}/${this.seats}`; }
  set summary(value) {
    const [plate, seats] = value.split('/');
    this.plate = plate;
    this.seats = Number(seats);
  }
  static fromLabel(label) { return new Example(label.trim()); }
  report = () => `${this.plate} has serial ${this.#serial}`;
}

const one = new Example('LAG-441-XA');
console.log(one.describe());
console.log('getter:', one.summary);

one.summary = 'ABJ-772-KM/11';
console.log('after setter:', one.describe());

const two = Example.fromLabel('   ENU-318-PH   ');
console.log(two.describe());
console.log('static count:', Example.built);

const detached = two.report;      // an arrow field survives detachment
console.log(detached());

Output


      

Field or constructor assignment?

Both put a property on the instance. Fields are declared at the top of the class, which documents the shape of an object at a glance. Use fields for defaults and constructor assignment for anything derived from the arguments.

Fields run first, then the constructor body

class Stop {
  town = 'unnamed';
  reached = false;
  history = [];        // a fresh array per instance, unlike a prototype property

  constructor(town) {
    console.log('at the top of the constructor, town is already', JSON.stringify(this.town));
    if (town) this.town = town;
  }
}

const a = new Stop('Ore');
const b = new Stop();

console.log(a.town, '/', b.town);
a.history.push('passed');
console.log('separate arrays:', a.history, b.history);

Output


      

Two things classes do that constructors did not

1. Calling one without new is an error, always

class Bus {
  constructor(plate) { this.plate = plate; }
}

try {
  const oops = Bus('LAG-441-XA');
  console.log(oops);
} catch (err) {
  console.log(err.name + ':', err.message);
}

Output


      

2. Class bodies are strict, and declarations are not hoisted

// A class body always runs in strict mode, even in a sloppy script.
class Strict {
  test() {
    // In sloppy mode this would silently create a global. Here it throws.
    try {
      undeclared = 1;
      return 'assigned silently';
    } catch (err) {
      return err.name + ': ' + err.message;
    }
  }
}
console.log(new Strict().test());

// And unlike function declarations, a class cannot be used before its line.
try {
  new TooEarly();
} catch (err) {
  console.log(err.name + ':', err.message);
}
class TooEarly {}

Output


      

Class expressions and toString

Classes are values, so they can be assigned and passed

// A class expression, optionally anonymous.
const Bus = class {
  constructor(plate) { this.plate = plate; }
  toString() { return `Bus(${this.plate})`; }
};

const b = new Bus('LAG-441-XA');
console.log(String(b));
console.log(`${b}`);
console.log('class name:', Bus.name);

// Defining toString changes how the object appears in text.
console.log('plain object for comparison:', String({ plate: 'x' }));

Output


      

Your turn: convert it

Below is a factory function. Rewrite it as a class called Ticket that behaves the same way. It needs four things:

  • a seat and a row;
  • a label getter that returns something like "D12";
  • a reserve() method that sets reserved to true and returns this.

Keep the factory too, and check that both produce the same output.

Editable. Convert it, then run it.

Output


      

Recap

  • class is shorthand for a constructor function plus methods on its prototype. Nothing new happens underneath.
  • Fields land on the instance and run before the constructor body; methods land on the prototype and are shared.
  • A class must be called with new, its body is always strict, and it cannot be used before its declaration.
  • Class bodies take no commas between members.
  • Define toString so your objects read well in logs and template literals.

Saved in this browser only.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.