// ───────────────────────────────────────────────────────────────────────────── // JS CLASSES, PROTOTYPES, `this` // ───────────────────────────────────────────────────────────────────────────── // CLASS : constructor, fields (public / #private / static), getters/setters, // static methods, extends + super, instanceof, toString/Symbol.iterator // THIS : decided by HOW a function is CALLED: obj.m() -> obj; m() -> undefined // (strict); new F() -> the new object; arrow fn -> lexical (enclosing) // `this`; .call/.apply/.bind set it explicitly. Callbacks lose `this` // unless arrow or bound. // PROTO : every object has [[Prototype]]; class methods live on C.prototype; // lookup walks the chain. Object.create(proto) builds one directly. // SAY : "prototype chain", "lexical this", "method borrowing", "composition // over inheritance", "encapsulation via #private", "polymorphism via // override", "mixin", "duck typing" // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── 1. A complete class class Account { static #count = 0; // static private static bankName = 'Obelisk'; // static public #balance = 0; // instance private history = []; // instance public field constructor(owner, opening = 0) { this.owner = owner; if (opening) this.deposit(opening); Account.#count++; } get balance() { return this.#balance; } // getter: read like a property set nickname(v) { this._nick = v.trim(); } // setter deposit(amt) { if (!(amt > 0)) throw new RangeError('amount must be positive'); this.#balance += amt; this.history.push(['deposit', amt]); return this; // chainable } withdraw(amt) { if (amt > this.#balance) return false; this.#balance -= amt; this.history.push(['withdraw', amt]); return true; } static count() { return Account.#count; } static from(json) { return new Account(json.owner, json.opening); } toString() { return `${this.owner}: ${this.#balance}`; } toJSON() { return { owner: this.owner, balance: this.#balance }; } *[Symbol.iterator]() { yield* this.history; } // makes `for (const h of acct)` work } const acct = new Account('ada', 100).deposit(50); assert.equal(acct.balance, 150); assert.equal(acct.withdraw(500), false); assert.throws(() => acct.deposit(-1), RangeError); assert.equal(Account.count(), 1); assert.equal(String(acct), 'ada: 150'); assert.equal(JSON.stringify(acct), '{"owner":"ada","balance":150}'); assert.deepEqual([...acct], [['deposit', 100], ['deposit', 50]]); assert.throws(() => { acct.balance = 999; }, TypeError); // getter-only: TypeError in strict mode, silent no-op in sloppy assert.equal(acct.balance, 150); // ── 2. Inheritance: extends / super / override / instanceof class Savings extends Account { constructor(owner, rate) { super(owner); this.rate = rate; } addInterest() { return this.deposit(this.balance * this.rate); } toString() { return `[savings] ${super.toString()}`; } } const sv = new Savings('bo', 0.1).deposit(100).addInterest(); assert.equal(sv.balance, 110); assert.equal(String(sv), '[savings] bo: 110'); assert.equal(sv instanceof Savings && sv instanceof Account, true); assert.equal(Object.getPrototypeOf(sv) === Savings.prototype, true); assert.equal(Object.getPrototypeOf(Savings.prototype) === Account.prototype, true); assert.equal(typeof Account, 'function'); // classes are functions underneath // ── 3. `this` — the four rules const counter = { n: 0, inc() { this.n++; return this.n; }, incLater() { return [1, 2].map(() => ++this.n); }, // arrow keeps `this` broken() { return [1].map(function () { return this; }); } // regular fn: `this` is undefined in strict }; assert.equal(counter.inc(), 1); // implicit binding: obj.m() assert.deepEqual(counter.incLater(), [2, 3]); assert.deepEqual(counter.broken(), [undefined]); const loose = counter.inc; // detached method loses `this` assert.throws(() => loose(), TypeError); const bound = counter.inc.bind(counter); // explicit binding assert.equal(bound(), 4); assert.equal(counter.inc.call({ n: 10 }), 11); // call(thisArg, ...args) assert.equal(counter.inc.apply({ n: 20 }, []), 21); // apply(thisArg, argsArray) class Btn { label = 'ok'; handler = () => this.label; } // arrow field = auto-bound method (React-style) const h = new Btn().handler; assert.equal(h(), 'ok'); // ── 4. Prototypes without class syntax (still asked) function Dog(name) { this.name = name; } // constructor function Dog.prototype.speak = function () { return `${this.name} barks`; }; const d = new Dog('rex'); assert.equal(d.speak(), 'rex barks'); assert.equal(Object.getPrototypeOf(d) === Dog.prototype, true); assert.equal(Object.hasOwn(d, 'speak'), false); // method lives on the prototype const proto = { hello() { return `hi ${this.who}`; } }; const made = Object.create(proto, { who: { value: 'x', enumerable: true } }); assert.equal(made.hello(), 'hi x'); // `new` does: create {} -> set [[Prototype]] -> call fn with this -> return this (unless fn returns an object) // ── 5. Composition / mixins (what you say when asked about deep hierarchies) const canLog = o => ({ ...o, log(msg) { return `[${o.id}] ${msg}`; } }); const canRetry = o => ({ ...o, retries: 3 }); const svc = canRetry(canLog({ id: 'svc' })); assert.equal(svc.log('up'), '[svc] up'); assert.equal(svc.retries, 3); const Serializable = Base => class extends Base { ser() { return JSON.stringify(this); } }; // class mixin class Point { constructor(x) { this.x = x; } } assert.equal(new (Serializable(Point))(1).ser(), '{"x":1}'); // ── 6. Static vs instance; class expressions; private checks class Id { static next = 1; static gen() { return Id.next++; } } assert.deepEqual([Id.gen(), Id.gen()], [1, 2]); const Anon = class { x = 1; }; assert.equal(new Anon().x, 1); class Secret { #s = 1; static is(o) { return #s in o; } } // brand check assert.equal(Secret.is(new Secret()), true); assert.equal(Secret.is({}), false); // ── 7. Abstract-ish / interface-ish patterns (JS has neither keyword) class Shape { constructor() { if (new.target === Shape) throw new TypeError('abstract'); } area() { throw new Error('not implemented'); } } class Sq extends Shape { constructor(s) { super(); this.s = s; } area() { return this.s ** 2; } } assert.throws(() => new Shape(), TypeError); assert.equal(new Sq(3).area(), 9); const implementsIface = (o, ...methods) => methods.every(m => typeof o[m] === 'function'); // duck typing assert.equal(implementsIface(new Sq(1), 'area'), true); console.log('ok: javascript/classes-this.js');