Static Members
Some things belong to the class itself rather than to any one object built from it. The timetable on the wall is not a bus.
By the end: Put counters, factories and constants where they belong.
The timetable pinned to the park wall is not a bus. It describes buses, it is shared by all of them, and asking it "how many seats do you have?" is a category error.
A static member belongs to the class itself, not to any instance built from it. You reach it through the class name.
class Bus {
static built = 0;
static MAX_SEATS = 59;
constructor(plate) {
this.plate = plate;
Bus.built++;
}
static census() {
return `${Bus.built} buses registered so far`;
}
}
new Bus('LAG-441-XA');
new Bus('ABJ-772-KM');
new Bus('ENU-318-PH');
console.log(Bus.census());
console.log('limit:', Bus.MAX_SEATS);
// Not on the instances. This is the whole point.
const b = new Bus('KAN-905-BD');
console.log('b.built:', b.built);
console.log('b.census:', b.census);
console.log(Bus.census());
Output
The four jobs statics actually do
class Departure {
constructor(route, minutes) {
this.route = route;
this.minutes = minutes; // minutes past midnight
}
static fromClock(route, clock) {
const [hours, mins] = clock.split(':').map(Number);
return new Departure(route, hours * 60 + mins);
}
static fromJSON(json) {
const data = JSON.parse(json);
return new Departure(data.route, data.minutes);
}
static firstRun() {
return new Departure('Lagos - Abuja', 5 * 60);
}
toString() {
const hours = Math.floor(this.minutes / 60);
const mins = String(this.minutes % 60).padStart(2, '0');
return `${this.route} departs ${hours}:${mins}`;
}
}
console.log(String(Departure.fromClock('Lagos - Abuja', '6:30')));
console.log(String(Departure.fromJSON('{"route":"Lagos - Kano","minutes":420}')));
console.log(String(Departure.firstRun()));
Output
class Seat {
static ROWS = Object.freeze(['A', 'B', 'C', 'D']);
static PRICE = Object.freeze({ economy: 12000, luxury: 18000 });
constructor(row, number, tier = 'economy') {
if (!Seat.ROWS.includes(row)) {
throw new RangeError(`Row ${row} does not exist. Rows are ${Seat.ROWS.join(', ')}.`);
}
this.row = row;
this.number = number;
this.tier = tier;
}
get price() { return Seat.PRICE[this.tier]; }
get label() { return `${this.row}${this.number}`; }
}
const s = new Seat('D', 12, 'luxury');
console.log(s.label, 'costs', s.price);
try { new Seat('Z', 1); } catch (err) { console.log('refused:', err.message); }
// Frozen, so a caller cannot redefine your price list. In strict mode the
// attempt throws rather than being quietly dropped.
try {
Seat.PRICE.economy = 1;
} catch (err) {
console.log(err.name + ':', err.message);
}
console.log('still:', Seat.PRICE.economy);
Output
class Clock {
static parse(text) {
const match = /^(\d{1,2}):(\d{2})$/.exec(text);
if (!match) throw new SyntaxError(`Not a time: "${text}"`);
return Number(match[1]) * 60 + Number(match[2]);
}
static format(minutes) {
const hours = Math.floor(minutes / 60);
const mins = String(minutes % 60).padStart(2, '0');
return `${hours}:${mins}`;
}
}
console.log(Clock.parse('12:30'));
console.log(Clock.format(750));
try { Clock.parse('12-30'); } catch (err) { console.log(err.name + ':', err.message); }
Output
class Trip {
static #all = [];
constructor(route) {
this.route = route;
Trip.#all.push(this);
}
static get count() { return Trip.#all.length; }
static find(route) {
return Trip.#all.find(t => t.route === route) ?? null;
}
static routes() {
return Trip.#all.map(t => t.route);
}
}
new Trip('Lagos - Abuja');
new Trip('Lagos - Kano');
console.log('count:', Trip.count);
console.log('routes:', Trip.routes());
console.log('found:', Trip.find('Lagos - Kano'));
console.log('missing:', Trip.find('Lagos - Jos'));
Output
this inside a static method
In a static method, this is the class. That is occasionally useful and occasionally the cause of a subtle bug with subclasses.
class Bus {
static built = 0;
static make(plate) {
// `this` here is whichever class the call was made on.
this.built++;
return new this(plate);
}
constructor(plate) { this.plate = plate; }
toString() { return `${this.constructor.name}(${this.plate})`; }
}
class LuxuryBus extends Bus {
static built = 0; // its own counter
}
const a = Bus.make('ABJ-772-KM');
const b = LuxuryBus.make('LAG-441-XA');
console.log(String(a), String(b));
console.log('Bus.built:', Bus.built);
console.log('LuxuryBus.built:', LuxuryBus.built);
console.log('b is a LuxuryBus:', b instanceof LuxuryBus, '| and a Bus:', b instanceof Bus);
Output
new this(plate) is the trick worth remembering. Because this is the class the call was made on, LuxuryBus.make() returns a LuxuryBus, not a Bus. Writing new Bus(plate) there would have quietly broken every subclass.
Statics are inherited by subclasses
class Base {
static tag = 'base';
static describe() { return `I am ${this.tag}`; }
}
class Middle extends Base {}
class Leaf extends Middle {
static tag = 'leaf';
}
console.log(Middle.describe()); // inherits both, so tag is still 'base'
console.log(Leaf.describe()); // inherits describe, overrides tag
console.log('Middle inherits from Base:', Object.getPrototypeOf(Middle) === Base);
Output
class Base {
static log = [];
static record(x) { this.log.push(x); }
}
class A extends Base {}
class B extends Base {}
A.record('from A');
B.record('from B');
console.log('A.log:', A.log);
console.log('B.log:', B.log);
console.log('same array?', A.log === B.log);
Output
Your turn: an id issuer
Write a Ticket class. Keep a private static counter. Every ticket gets a sequential id like T-001, assigned in the constructor, exposed read-only. Add a static issued getter and a static reset(). Then make three tickets and check the ids.
Output
Recap
- A static member belongs to the class, not to instances, and is reached through the class name.
- Instances cannot see statics; from inside a method use
ClassName.thingorthis.constructor.thing. - Statics earn their keep as named constructors, concept-level constants, stateless utilities, and private registries.
- Inside a static method
thisis the class, sonew this(...)keeps subclasses working. - Subclasses inherit statics, and an object-valued static is shared until a subclass declares its own.