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.
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.
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
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';
}
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.
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
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
// 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
// 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
seatand arow; - a
labelgetter that returns something like"D12"; - a
reserve()method that setsreservedto true and returnsthis.
Keep the factory too, and check that both produce the same output.
Output
Recap
classis 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
toStringso your objects read well in logs and template literals.