Emmanuel Akinfulubi

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.

Object literal

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.

Read a property

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.

Factory function

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.

Constructor function

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

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.

Object.create

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.

The four steps

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.

The five call forms

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.

Fixing a detached method

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.

Inspecting the chain

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.

Own versus inherited

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.

The shared-array trap

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.

All nine forms

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.

Private fields

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.

Read-only and validated

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.

Hand out copies

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.

extends and super

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.

Abstract, by hand

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.

Custom errors

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.

Static in a subclass

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.

Mixins

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.

Replace a type switch

// 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.

Language hooks

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.

Capability check

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.

has-a instead of is-a

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.

Deliberate delegation

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.

Factory method

class Departure {
  static fromClock(route, hhmm) { /* ... */ }
  static fromJSON(json) { /* ... */ }
}

Strategy

Inject the algorithm; a new one is a new object, not an edit.

Strategy

new Basket(standardPricing);
basket.using(memberPricing).total;

Observer

Copy the listener set before iterating, and return an unsubscribe function from on().

Observer

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.

Adapter

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.

Dependency injection

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.

The five

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.

Three depths of copy

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().

Object equality

{ a: 1 } === { a: 1 };        // false
seat.equals(other);            // your own rule

Newsletter

New writing, when it lands.

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