Cheatsheet
Every syntax form in the course on one page, each with the mistake it invites written next to it. Print it if that helps; the stylesheet has a print rule that drops the navigation and the run buttons.
Making objects
Object literal
The fastest way to describe one thing. No ceremony needed.
const bus = {
plate: 'LAG-441-XA',
seatsLeft: 9,
describe() { return this.plate; },
};
Read a property
Use the dot unless the key is in a variable or is not a valid identifier.
bus.plate;
bus['plate'];
const key = 'seatsLeft';
bus[key];
bus.owner?.town; // undefined instead of throwing
Factory function
Closure variables are genuinely private. Cost: one copy of each method per object.
function makeBus(plate, seatsLeft) {
let booked = 0; // private
return {
plate,
seatsLeft,
book(n) { booked += n; this.seatsLeft -= n; return this; },
bookingsMade() { return booked; },
};
}
Constructor function
Capitalised by convention only. Without new in strict mode it throws.
function Bus(plate) {
if (!new.target) throw new TypeError('use new');
this.plate = plate;
}
Bus.prototype.describe = function () { return this.plate; };
const b = new Bus('LAG-441-XA');
Class
No commas between members. Cannot be called without new. Not hoisted.
class Bus {
constructor(plate) { this.plate = plate; }
describe() { return this.plate; }
}
Object.create
Sets the prototype directly, no constructor involved. Object.create(null) has no prototype at all.
const behaviour = { describe() { return this.plate; } };
const b = Object.create(behaviour);
b.plate = 'LAG-441-XA';
What new does
The four steps
A returned object overrides the new one; a returned primitive is ignored.
function pretendNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype); // 1 and 2
const out = Ctor.apply(obj, args); // 3
return (typeof out === 'object' && out !== null) ? out : obj; // 4
}
this
The five call forms
Decided at the call, never at the definition. Look to the left of the dot.
obj.fn(); // this === obj
const f = obj.fn; f(); // this === undefined (strict)
new Ctor(); // this === the new object
fn.call(x); fn.apply(x, []); fn.bind(x);
() => this; // no own this: borrowed from where it was written
Fixing a detached method
Each .bind() returns a NEW function, so store it if you need to remove a listener later.
el.addEventListener('click', () => obj.fn()); // clearest
const bound = obj.fn.bind(obj); // reusable reference
class C { fn = () => this; } // bound at construction
Prototypes
Inspecting the chain
Ctor.prototype is what instances hang from. It is not the prototype OF Ctor.
Object.getPrototypeOf(b) === Bus.prototype;
Object.getPrototypeOf(Bus) === Function.prototype;
Bus.prototype.constructor === Bus;
b instanceof Bus; // is Bus.prototype on b's chain?
Own versus inherited
Assignment always writes to the object. Mutation reaches through to the shared value.
Object.hasOwn(b, 'kind'); // only the object itself
'kind' in b; // the whole chain
Object.keys(b); // own enumerable only
Object.getOwnPropertyNames(b); // own, including non-enumerable
The shared-array trap
Never put a mutable value on a prototype. Per-instance state goes in the constructor or a field.
Bus.prototype.faults = []; // one array for every instance
// instead:
function Bus(plate) { this.plate = plate; this.faults = []; }
Class members
All nine forms
Fields run before the constructor body. Arrow fields are per instance and cannot be detached.
class Kit {
parts = 0; // public field -> instance
#serial = 'K-1'; // private field -> instance, unreachable
static made = 0; // static field -> the class
static #log = []; // private static
constructor(name) { this.name = name; Kit.made++; }
add(n) { this.parts += n; return this; } // method -> prototype
get summary() { return this.parts; } // getter
set summary(v) { this.parts = Number(v); } // setter
static blank() { return new this('blank'); } // static method
stamp = () => this.#serial; // arrow field, bound
}
Private fields
obj.#x outside the class is a SyntaxError. Invisible to keys, spread and JSON.
class A {
#x = 1;
static isA(v) { return #x in v; } // brand check nothing can forge
}
Object.keys(new A()); // []
JSON.stringify(new A()); // "{}"
Read-only and validated
Never assign to the same name inside its own setter: infinite recursion.
class T {
#c = 0;
get celsius() { return this.#c; }
set celsius(v) {
if (v < -273.15) throw new RangeError('below absolute zero');
this.#c = v;
}
get fahrenheit() { return this.#c * 9 / 5 + 32; }
set fahrenheit(v) { this.celsius = (v - 32) * 5 / 9; }
}
Hand out copies
A getter returning the real array undoes your encapsulation.
get items() { return [...this.#items]; }
get config() { return structuredClone(this.#config); }
Inheritance
extends and super
super() must run before you touch this, and it must run at all if you write a constructor.
class LuxuryBus extends Bus {
constructor(plate, extra) {
super(plate); // first
this.extra = extra;
}
describe() { return super.describe() + ' / ' + this.extra; }
checklist() { return [...super.checklist(), 'test the air conditioning']; }
}
Abstract, by hand
There is no abstract keyword. new.target is the constructor new was called on.
class Vehicle {
constructor() {
if (new.target === Vehicle) throw new TypeError('Vehicle is abstract');
}
load() { throw new Error(this.constructor.name + ' must implement load'); }
}
Custom errors
Always set name, and prefer catching by instanceof over matching message text.
class FullyBookedError extends Error {
constructor(route) {
super(route + ' is fully booked.');
this.name = 'FullyBookedError';
this.route = route;
}
}
Static in a subclass
new this() keeps subclasses working. An object-valued static is shared until redeclared.
class Base {
static make(...a) { return new this(...a); } // not new Base()
static log = []; // shared by subclasses!
}
class Child extends Base { static log = []; } // its own
Mixins
Still inheritance: each mixin adds a chain link and name clashes are silent.
const Serialisable = (Base) => class extends Base {
toJSON() { return { ...this, type: this.constructor.name }; }
};
class Departure extends Serialisable(Object) {}
Polymorphism
Replace a type switch
No method overloading exists: a second definition of the same name replaces the first.
// instead of switch (x.type)
class Ticket { cost() { return 12000; } }
class Luggage { cost() { return 500; } }
lines.reduce((n, l) => n + l.cost(), 0);
Language hooks
toString is used by template literals and concatenation; Symbol.iterator by for...of and spread.
class Money { toString() { return '₦12000'; } }
class Fleet {
#m = [];
*[Symbol.iterator]() { yield* this.#m; }
}
[...new Fleet()];
Capability check
Ask whether it responds, not what class it is.
const can = (o, m) => typeof o[m] === 'function';
if (can(thing, 'prepare')) thing.prepare();
Composition
has-a instead of is-a
Two independent axes of variation means composition. One axis may justify inheritance.
class Bus {
#parts = new Map();
fit(label, part) { this.#parts.set(label, part); return this; }
prep() { return [...this.#parts.values()].map(p => p.prep()); }
}
new Bus('x').fit('aircon', new AirCon(9)).fit('rack', new RoofRack(400));
Deliberate delegation
Forwarding by hand is more typing, and buys you control over what callers can reach.
class Booking {
#ledger = new Ledger();
get total() { return this.#ledger.total; }
// Ledger.clear() is deliberately NOT exposed
}
Patterns
Factory method
Several named constructors beat one constructor with mode arguments.
class Departure {
static fromClock(route, hhmm) { /* ... */ }
static fromJSON(json) { /* ... */ }
}
Strategy
Inject the algorithm; a new one is a new object, not an edit.
new Basket(standardPricing);
basket.using(memberPricing).total;
Observer
Copy the listener set before iterating, and return an unsubscribe function from on().
const off = trip.on('booked', d => console.log(d));
trip.emit('booked', { seat: '1A' });
off();
Adapter
The cheapest way to stop a third-party API spreading through your code.
class LegacyAdapter {
save(k, v) { legacy.put(k, JSON.stringify(v)); return this; }
load(k) { const j = legacy.fetch(k); return j ? JSON.parse(j) : null; }
}
Dependency injection
Passing the clock in is what makes a time-dependent class testable.
class Session {
#now;
constructor({ now = () => Date.now() } = {}) { this.#now = now; }
}
new Session({ now: () => 1000 });
SOLID, in one line each
The five
Each one is recognisable by shape, which is what makes them useful in review.
S one reason to change per class
O add a case by adding code, not editing code
L a subclass must work wherever the parent works
I nothing forced to implement what it does not need
D depend on the interface; inject the concrete thing
Copying and equality
Three depths of copy
Assignment shares. Spread copies one level. structuredClone copies all levels but drops functions.
const share = obj;
const shallow = { ...obj };
const deep = structuredClone(obj);
Object.freeze(obj); // shallow freeze only
Object equality
Two objects are only === when they are the same object. Give value objects an equals().
{ a: 1 } === { a: 1 }; // false
seat.equals(other); // your own rule