Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

JavaScript (ES2026)  Module 11 – Prototypes and classes

Composition, mixins and OOP design in JavaScript

How to compose objects from small collaborators instead of deep class trees, write mixins with Object.assign or subclass factories, and apply OOP ideas.

  • Intermediate
  • 22 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 22 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Compose behaviour from small objects and functions instead of deep inheritance
  • Write mixins with Object.assign and with subclass factories, and predict where they conflict
  • Pass collaborators such as clocks and senders into objects so that tests can use fakes
  • Apply encapsulation, polymorphism and SOLID-style ideas the way JavaScript expresses them

Before you start

What you’ll learn · Before you start

What you will learn

  • Compose behaviour from small objects and functions instead of deep inheritance
  • Write mixins with Object.assign and with subclass factories, and predict where they conflict
  • Pass collaborators such as clocks and senders into objects so that tests can use fakes
  • Apply encapsulation, polymorphism and SOLID-style ideas the way JavaScript expresses them

Before you start

On this page

Inheritance says what an object is; composition says what it has. In JavaScript, deep class hierarchies age badly: a change to a base class reaches every subclass, and every new combination of features needs another subclass. Composition builds an object from small collaborators that it holds and calls, such as a sender, a clock or a log, so that each part can be swapped, reused and replaced by a fake in a test. Mixins give a set of methods to classes that inheritance does not relate, either by copying methods onto a prototype with Object.assign or with a subclass factory, (Base) => class extends Base { … }. And because JavaScript looks methods up when they are called, any object with the right methods works where another is expected, which is polymorphism without interfaces. This lesson turns a class hierarchy into composed parts, writes both kinds of mixin with the traps of each, and shows how encapsulation, polymorphism and the usual design principles look in idiomatic JavaScript.

When inheritance stops fitting

Here are notifications built the inheritance way, one feature per level:

Notifier, SmsNotifier, RetryingSmsNotifier, and how the classes multiply JavaScript · hierarchy.mjs
// Notifications built by inheritance: each new feature becomes another level of subclass.
const outbox = [];

class Notifier {
  notify(user, text) {
    this.deliver(user, text);
  }

  deliver() {
    throw new Error('a subclass must implement deliver');
  }
}

class SmsNotifier extends Notifier {
  deliver(user, text) {
    outbox.push(`sms to ${user.phone}: ${text}`);
  }
}

class RetryingSmsNotifier extends SmsNotifier {
  notify(user, text) {
    for (let attempt = 1; ; attempt++) {
      try {
        return super.notify(user, text);
      } catch (err) {
        if (attempt === 3) throw err;
      }
    }
  }
}

new RetryingSmsNotifier().notify({ phone: '98xxxxxx01' }, 'Your order is on its way');
console.log(outbox);

// Retrying email needs RetryingEmailNotifier, which repeats the retry loop; quiet hours need another level on top
// of every class. With one subclass per combination, the count multiplies; with parts, it adds up.
console.log('channels  optional features  subclasses for every combination  parts to compose');
for (const [channels, features] of [[2, 1], [3, 2], [3, 3], [4, 4]]) {
  console.log(String(channels).padEnd(10) + String(features).padEnd(19) + String(channels * 2 ** features).padEnd(34) + (channels + features));
}

Output

[ 'sms to 98xxxxxx01: Your order is on its way' ]
channels  optional features  subclasses for every combination  parts to compose
2         1                  4                                 3
3         2                  12                                5
3         3                  24                                6
4         4                  64                                8

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node hierarchy.mjs

The code works, and its problems show up as soon as it grows:

  • Combinations multiply. Retrying belongs to RetryingSmsNotifier, so email that retries needs a second class with the same loop, and quiet hours need a further level on top of each. With one subclass per combination, every optional feature doubles the count: three channels with three features need 24 classes, against six parts to compose.
  • Subclasses depend on their parent’s insides. RetryingSmsNotifier overrides notify and relies on the parent’s notify calling deliver exactly once. A harmless-looking change to Notifier, such as logging before deliver, changes the behaviour of every subclass.
  • Tests have to go through the hierarchy. Retry can only be tested together with SMS, and there is no place to give the classes a clock or a fake gateway.

Inheritance still fits a real “is a” relationship with a short chain and a shared contract: Error subclasses, a custom element that must extend HTMLElement, a small family of classes that all keep their parent’s promises.

Composition: an object that holds its parts

The same behaviour, built from parts that the notifier is given:

Inheritance: four classes in a chain, one per feature. Composition: a notifier calling two senders wrapped with retry and quiet hours.Inheritance:a subclass per combinationComposition:a notifier holds its partsNotifierSmsNotifierRetryingSmsNotifierQuietHoursRetryingSmsNotifierNotifierholds senders and a logwithQuietHours( withRetry(smsSender), clock)withRetry( emailSender)extended byextended byextended bycallscalls

The same notifications, as a class chain and as composed parts

Text description of the diagram

Two groups, one above the other.

The first group, inheritance, is a chain of four classes, each extended by the next: Notifier, SmsNotifier, RetryingSmsNotifier and QuietHoursRetryingSmsNotifier. Each feature is another level, and email with the same features would need a second chain.

The second group, composition, is one Notifier that holds a list of senders and a log, and calls each sender. The first sender is smsSender wrapped in withRetry and then in withQuietHours, which is given a clock. The second is emailSender wrapped in withRetry. The same two wrappers work for any sender, and the clock can be a fake in tests.

A notifier composed from senders, retries, quiet hours, a clock and a log JavaScript · notifier.mjs
// The same notifications composed from parts: senders with the same two methods, wrappers that add a feature to any
// sender, and a notifier that is given its collaborators instead of creating them.
const outbox = [];

const smsSender = {
  name: 'sms',
  canReach: (user) => Boolean(user.phone),
  send: (user, text) => outbox.push(`sms to ${user.phone}: ${text}`),
};
const emailSender = {
  name: 'email',
  canReach: (user) => Boolean(user.email),
  send: (user, text) => outbox.push(`email to ${user.email}: ${text}`),
};

// A sender that fails twice and then works, like a gateway having a bad minute.
function flakySender(sender, failures) {
  let left = failures;
  return {
    ...sender,
    send(user, text) {
      if (left-- > 0) throw new Error(`${sender.name} gateway timed out`);
      return sender.send(user, text);
    },
  };
}

// Features as wrappers: each one works with any sender.
function withRetry(sender, attempts = 3) {
  return {
    ...sender,
    send(user, text) {
      for (let attempt = 1; ; attempt++) {
        try {
          return sender.send(user, text);
        } catch (err) {
          if (attempt >= attempts) throw err;
        }
      }
    },
  };
}
function withQuietHours(sender, clock, { from = 22, to = 7 } = {}) {
  const quiet = () => clock.hour() >= from || clock.hour() < to;
  return { ...sender, canReach: (user) => !quiet() && sender.canReach(user) };
}

class Notifier {
  #senders;
  #log;

  constructor({ senders, log }) {
    this.#senders = senders;
    this.#log = log;
  }

  notify(user, text) {
    const used = this.#senders.filter((s) => s.canReach(user));
    for (const s of used) s.send(user, text);
    this.#log(`${user.name}: ${used.map((s) => s.name).join(', ') || 'nobody yet'}`);
    return used.length;
  }
}

// A fake clock and a log that collects lines: the tests decide the time and read what happened.
const clock = { now: 14, hour() { return this.now; } };
const lines = [];
const notifier = new Notifier({
  senders: [withQuietHours(withRetry(flakySender(smsSender, 2)), clock), withRetry(emailSender)],
  log: (line) => lines.push(line),
});

notifier.notify({ name: 'Asha', phone: '98xxxxxx01' }, 'Order shipped');
clock.now = 23;
notifier.notify({ name: 'Ravi', phone: '98xxxxxx02', email: 'ravi@example.com' }, 'Order shipped');
notifier.notify({ name: 'Meera', phone: '98xxxxxx03' }, 'Order shipped');
console.log(outbox);
console.log(lines);

Output

[
  'sms to 98xxxxxx01: Order shipped',
  'email to ravi@example.com: Order shipped'
]
[ 'Asha: sms', 'Ravi: email', 'Meera: nobody yet' ]

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node notifier.mjs

  • Senders are plain objects with two methods. Notifier only calls canReach and send, so anything that has them is a sender: an SMS gateway, an email client, an in-app inbox or a fake that records messages. That is polymorphism by duck typing, with no base class and no interface declaration.
  • Features are wrappers. withRetry and withQuietHours take any sender and return a sender with one behaviour added, so they combine freely, and email got its retries without new code. This is the decorator pattern.
  • Collaborators are passed in. Notifier receives its senders and its log, and withQuietHours receives a clock. Passing dependencies in rather than creating them inside, often called dependency injection, is what let the run use a clock set to 14:00 and then 23:00, and a sender that fails twice, without waiting for the night or touching any shared object.
  • The parts stay private. Notifier keeps them in #private fields, so the only way to use it is notify.

The cost is a little more wiring at the place where the objects are created. In exchange, each part is small enough to read in one screen and to test on its own.

Mixins with Object.assign

A mixin is a set of methods that several unrelated classes share. The oldest way to apply one copies its methods onto a class’s prototype:

Object.assign mixins: overwrites, getters and class methods JavaScript · assign-mixin.mjs
// Mixins copied with Object.assign: what is copied, what is not, and what is silently overwritten.
const describable = {
  describe() {
    return `${this.constructor.name} ${this.id}`;
  },
  get label() {
    console.log('(label getter ran, this =', this === describable ? 'the mixin object)' : 'an order)');
    return `#${this.id}`;
  },
};

class Order {
  constructor(id) {
    this.id = id;
  }

  describe() {
    return 'the class method';
  }
}

Object.assign(Order.prototype, describable);
const order = new Order('A-17');
console.log(order.describe()); // the mixin's method replaced the class's own, without a warning
console.log(order.label); // a value computed once, during the copy, from the mixin object

// The fix for accessors: copy property descriptors instead of values.
class Invoice {
  constructor(id) {
    this.id = id;
  }
}
Object.defineProperties(Invoice.prototype, Object.getOwnPropertyDescriptors(describable));
console.log(new Invoice('B-2').label);

// Class methods are not enumerable, so Object.assign copies none of them from a class prototype.
class Greeter {
  greet() {
    return 'namaste';
  }
}
const target = {};
Object.assign(target, Greeter.prototype);
console.log('copied from a class prototype:', Object.keys(target));

Output

(label getter ran, this = the mixin object)
Order A-17
#undefined
(label getter ran, this = an order)
#B-2
copied from a class prototype: []

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node assign-mixin.mjs

Object.assign reads each own enumerable property of the mixin and assigns it to the target, and that causes three problems:

  1. It overwrites without a warning. The mixin’s describe replaced the method that Order declared itself, and the class never knew.
  2. Getters run once, during the copy. label was read from the mixin object, with the mixin as this, and the string "#undefined" was stored on the prototype. Object.defineProperties(target, Object.getOwnPropertyDescriptors(mixin)) copies the getter itself, as the Invoice line shows.
  3. Class methods are not enumerable, so Object.assign(target, Greeter.prototype) copies nothing at all.

A copied method that uses super keeps the object it was written in as its home, so super.describe() inside it looks at the mixin object’s prototype, not at the class it was copied to. Methods that need super need the next kind of mixin.

Subclass factories

A subclass factory is a function that takes a class and returns a subclass of it:

Timestamped and Serializable applied to one class JavaScript · subclass-factory.mjs
// Subclass-factory mixins: functions that take a class and return a subclass of it, so super calls chain through
// every mixin that was applied.
let tick = 0;
const clock = () => ++tick; // a counter instead of the time, so every run prints the same

const Timestamped = (Base) =>
  class Timestamped extends Base {
    #createdAt = clock();

    get createdAt() {
      return this.#createdAt;
    }

    describe() {
      return `${super.describe()} (created at tick ${this.#createdAt})`;
    }
  };

const Serializable = (Base) =>
  class Serializable extends Base {
    toJSON() {
      return { type: this.constructor.name, ...this };
    }

    describe() {
      return `[${super.describe()}]`;
    }
  };

class Entity {
  constructor(id) {
    this.id = id;
  }

  describe() {
    return `${this.constructor.name} ${this.id}`;
  }
}

class Order extends Timestamped(Serializable(Entity)) {}

const order = new Order('A-17');
console.log(order.describe());
console.log(JSON.stringify(order));

const chain = [];
for (let p = Object.getPrototypeOf(order); p !== null; p = Object.getPrototypeOf(p)) chain.push(p.constructor.name);
console.log(chain.join(' -> '));

// The mixin is an arrow function, which has no prototype, so instanceof cannot use it.
try {
  console.log(order instanceof Timestamped);
} catch (err) {
  console.log('order instanceof Timestamped:', err.name);
}

// Every call makes a new class, with new private names: two applications are not the same class.
const A = Timestamped(Entity);
const B = Timestamped(Entity);
console.log(A === B, new A('x') instanceof B);

Output

[Order A-17] (created at tick 1)
{"type":"Order","id":"A-17"}
Order -> Timestamped -> Serializable -> Entity -> Object
order instanceof Timestamped: TypeError
false false

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node subclass-factory.mjs

Timestamped(Serializable(Entity)) builds a real prototype chain, Order → Timestamped → Serializable → Entity → Object, so everything that works for classes works here:

  • super calls chain through every mixin. Each describe wraps the one below it, so the order of application decides the result: the outermost mixin is nearest to Order and runs first.
  • Mixins can have fields, constructors and private state, such as the #createdAt field that Timestamped adds to each order.
  • Name the class expression. class Timestamped extends Base gives the chain readable names in logs and stack traces, where an anonymous class would show no name at all.

And it has traps of its own:

  • order instanceof Timestamped throws a TypeError, because the mixin is an arrow function without a prototype. Offer an isTimestamped(value) function, or give the mixin a static Symbol.hasInstance method that checks a brand its classes set.
  • Every call makes a new class. Timestamped(Entity) twice gives two different classes with different private names, so objects of one do not count as objects of the other.
  • Two mixins with the same method name resolve by order: the outer one wins. It should call super.method() when it means to extend the method rather than replace it.

Two or three mixins on a class are easy to follow. A longer chain is a sign that the class wants to hold parts instead.

Object-oriented design the JavaScript way

The usual object-oriented ideas all apply in JavaScript, but they look different from their Java or C# versions:

  • Encapsulation: #private fields and methods, closures, and modules that export only their public functions.
  • Polymorphism: duck typing. Any object with canReach and send is a sender, and no interface keyword is needed.
  • Single responsibility: a sender sends, a wrapper adds one feature, and the notifier decides who is told.
  • Open for extension: a new channel is a new sender and a new feature is a new wrapper; Notifier does not change.
  • Substitutability: a subclass or a fake keeps every promise of what it replaces, which the SortedArray of Extending built-ins failed to do.
  • Small interfaces: collaborators ask for two methods, not for a whole class.
  • Dependency inversion: objects receive their collaborators, through constructor options or function parameters, instead of creating them.

These are design principles first, and the LLD track treats each one at the design level, with diagrams and case studies: single responsibility, open/closed, Liskov substitution, interface segregation and dependency inversion.

JavaScript & TypeScript Online Compiler Wrap the notifier's email sender in withQuietHours too, and give it a clock that says 23.

Exercise: from a class hierarchy to composed rules

Exercise · Medium · JavaScript

Turn a fare class hierarchy into composed rules

A cab app prices rides with three classes, each extending the last: Fare charges 25 rupees plus a rate for every started kilometre, NightFare adds 25 % (rounded to the nearest rupee) from 22:00 to 05:59, and AirportNightFare adds 100 for an airport trip. The classes read the hour from a shared systemClock, so a test can only change the time by replacing systemClock.hour for everyone.

Keep the three classes and systemClock as they are, and add the same behaviour built from parts:

  • nightRule(amount, context) returns amount raised by 25 % and rounded when context.hour is 22, 23 or 0 to 5, and amount unchanged otherwise.
  • airportRule(amount, context) returns amount + 100.
  • createFare({ ratePerKm = 12, rules = [], clock = systemClock } = {}) returns an object with price(km). It computes 25 + Math.ceil(km) * ratePerKm, then passes the amount through each rule in order, as amount = rule(amount, { hour: clock.hour(), km }), and returns the result.

createFare({ rules: [nightRule, airportRule] }) must price every ride exactly as new AirportNightFare() does, and a rule is any function with that shape, so new rules need no new class:

const nightAtAirport = createFare({ rules: [nightRule, airportRule], clock: { hour: () => 23 } });
nightAtAirport.price(3);  // 25 + 3 × 12 = 61, then 76 at night, then 176
createFare().price(0.5);  // 37: a started kilometre counts as a whole one

The starter's createFare and rules are not written yet: run the sample tests to see what they expect.

Starter code · fares.mjs

/** The hour of day, 0 to 23, shared by every fare class below. */
export const systemClock = { hour: () => new Date().getHours() };

// The existing code: three classes, each extending the last. Keep them as they are.
export class Fare {
  constructor(ratePerKm = 12) {
    this.ratePerKm = ratePerKm;
  }

  price(km) {
    return 25 + Math.ceil(km) * this.ratePerKm;
  }
}

export class NightFare extends Fare {
  price(km) {
    const amount = super.price(km);
    const hour = systemClock.hour();
    return hour >= 22 || hour < 6 ? Math.round(amount * 1.25) : amount;
  }
}

export class AirportNightFare extends NightFare {
  price(km) {
    return super.price(km) + 100;
  }
}

// Write these three: the same fares, built from rules and a clock that are passed in.
export function nightRule(amount, context) {
  throw new Error('nightRule is not written yet');
}

export function airportRule(amount, context) {
  throw new Error('airportRule is not written yet');
}

export function createFare(options = {}) {
  throw new Error('createFare is not written yet');
}
The sample tests · fares.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { AirportNightFare, Fare, NightFare, airportRule, createFare, nightRule, systemClock } from './fares.mjs';

const at = (hour) => ({ hour: () => hour });

test('the distance price, with every started kilometre charged', () => {
  const fare = createFare();
  assert.equal(fare.price(0.5), 37);
  assert.equal(fare.price(3), 61);
  assert.equal(fare.price(3.2), 73);
  assert.equal(createFare({ ratePerKm: 15 }).price(2), 55);
});

test('nightRule raises fares from 22:00 to 05:59 only', () => {
  for (const hour of [22, 23, 0, 3, 5]) assert.equal(nightRule(61, { hour, km: 3 }), 76, `hour ${hour}`);
  for (const hour of [6, 12, 21]) assert.equal(nightRule(61, { hour, km: 3 }), 61, `hour ${hour}`);
  assert.equal(nightRule(37, { hour: 23, km: 1 }), 46);
  assert.equal(airportRule(61, { hour: 12, km: 3 }), 161);
});

test('the injected clock decides the hour', () => {
  const night = createFare({ rules: [nightRule], clock: at(23) });
  const day = createFare({ rules: [nightRule], clock: at(9) });
  assert.equal(night.price(3), 76);
  assert.equal(day.price(3), 61);
});

test('rules apply in the order given', () => {
  assert.equal(createFare({ rules: [nightRule, airportRule], clock: at(23) }).price(3), 176);
  assert.equal(createFare({ rules: [airportRule, nightRule], clock: at(23) }).price(3), 201);
  assert.equal(createFare({ rules: [nightRule, airportRule], clock: at(14) }).price(3), 161);
});

test('any function with the same shape is a rule', () => {
  const festivalDiscount = (amount) => amount - 10;
  const longTrip = (amount, { km }) => (km > 20 ? amount + 50 : amount);
  assert.equal(createFare({ rules: [festivalDiscount], clock: at(12) }).price(3), 51);
  assert.equal(createFare({ rules: [longTrip], clock: at(12) }).price(25), 375);
  assert.equal(createFare({ rules: [longTrip], clock: at(12) }).price(5), 85);
});

test('prices every ride exactly as the three classes do', () => {
  const realHour = systemClock.hour;
  try {
    for (const hour of [0, 5, 6, 12, 21, 22, 23]) {
      systemClock.hour = () => hour;
      for (const km of [0.4, 1, 2.5, 7, 19.9]) {
        assert.equal(createFare().price(km), new Fare().price(km), `Fare at ${hour}:00 for ${km} km`);
        assert.equal(createFare({ rules: [nightRule] }).price(km), new NightFare().price(km), `NightFare at ${hour}:00 for ${km} km`);
        assert.equal(createFare({ rules: [nightRule, airportRule] }).price(km), new AirportNightFare().price(km), `AirportNightFare at ${hour}:00 for ${km} km`);
      }
    }
  } finally {
    systemClock.hour = realHour;
  }
});

test('fares do not share their parts', () => {
  const a = createFare({ ratePerKm: 10, rules: [nightRule], clock: at(23) });
  const b = createFare({ ratePerKm: 20, clock: at(23) });
  assert.equal(a.price(1), 44);
  assert.equal(b.price(1), 45);
});
A hint

Write the two rules first: they are plain functions that take an amount and a context and return an amount, so they need no class. In createFare, keep the options in variables, and in price(km) read the hour once, build the context object, and apply the rules with rules.reduce((amount, rule) => rule(amount, context), start), where start is the distance price.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Self-check

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 In the composed notifier, email should now retry three times as well. What changes?

    Choose one answer.

    Show the answer to question 1

    Answer: Pass withRetry(emailSender) in the senders list; Notifier and the other senders stay as they are

    withRetry works with any object that has a send method, so a new combination is a new arrangement of existing parts. The notifier only calls canReach and send, and it does not need to know which features a sender has.

  2. Question 2 of 6 Object.assign(target, Greeter.prototype) copies nothing, although Greeter has a greet method. Why?

    Choose one answer.

    Show the answer to question 2

    Answer: Methods defined in a class body are not enumerable, and Object.assign copies only own enumerable properties

    Class syntax defines methods as non-enumerable properties of the prototype. Object.assign reads only own enumerable properties, so it finds nothing to copy. Object.getOwnPropertyDescriptors with Object.defineProperties copies them, but a subclass factory is usually the better mixin for classes.

  3. Question 3 of 6 assign-mixin.mjs copies a mixin with a label getter in two ways. What does it print?

    What does this program print? Choose one answer.

    // Mixins copied with Object.assign: what is copied, what is not, and what is silently overwritten.
    const describable = {
      describe() {
        return `${this.constructor.name} ${this.id}`;
      },
      get label() {
        console.log('(label getter ran, this =', this === describable ? 'the mixin object)' : 'an order)');
        return `#${this.id}`;
      },
    };
    
    class Order {
      constructor(id) {
        this.id = id;
      }
    
      describe() {
        return 'the class method';
      }
    }
    
    Object.assign(Order.prototype, describable);
    const order = new Order('A-17');
    console.log(order.describe()); // the mixin's method replaced the class's own, without a warning
    console.log(order.label); // a value computed once, during the copy, from the mixin object
    
    // The fix for accessors: copy property descriptors instead of values.
    class Invoice {
      constructor(id) {
        this.id = id;
      }
    }
    Object.defineProperties(Invoice.prototype, Object.getOwnPropertyDescriptors(describable));
    console.log(new Invoice('B-2').label);
    
    // Class methods are not enumerable, so Object.assign copies none of them from a class prototype.
    class Greeter {
      greet() {
        return 'namaste';
      }
    }
    const target = {};
    Object.assign(target, Greeter.prototype);
    console.log('copied from a class prototype:', Object.keys(target));
    Show the answer to question 3

    Answer: it prints

    (label getter ran, this = the mixin object)
    Order A-17
    #undefined
    (label getter ran, this = an order)
    #B-2
    copied from a class prototype: []

    Object.assign reads the getter once, during the copy, with the mixin object as this, so Order's label is the string "#undefined" from then on, and the mixin's describe silently replaces the class's own. Copying property descriptors keeps label a getter, which runs on each read with the invoice as this. The class prototype has no enumerable methods to copy.

  4. Question 4 of 6 Using the mixins of subclass-factory.mjs, what does the last line print?

    Read the code, then choose one answer.

    class Order extends Serializable(Timestamped(Entity)) {}
    console.log(new Order('A-17').describe());
    Show the answer to question 4

    Answer: [Order A-17 (created at tick 1)]

    The outermost mixin is nearest to Order on the prototype chain, so Serializable's describe runs first and wraps whatever super.describe() returns. That is Timestamped's describe, which adds the tick to Entity's text. Swapping the mixins swaps the wrapping, which is why the lesson's version prints the bracket before the tick.

  5. Question 5 of 6 Why does order instanceof Timestamped throw a TypeError, when Timestamped is the arrow function (Base) => class extends Base {…}?

    Choose one answer.

    Show the answer to question 5

    Answer: An arrow function has no prototype property, and instanceof needs one, or a Symbol.hasInstance method

    instanceof calls the default Symbol.hasInstance method, which reads the right side's prototype property and throws when it is not an object. A mixin that should answer instanceof needs a static Symbol.hasInstance method that checks a brand its classes set, or a separate isX function.

  6. Question 6 of 6 Which choices let a test run the notifier at 23:00 without waiting for the night or changing shared objects? Choose every one.

    Choose every answer that is right.

    Show the answer to question 6

    Answer:

    • Pass a clock object to withQuietHours
    • Pass the senders to the Notifier constructor

    Collaborators that are passed in can be fakes: a clock that says 23 and senders that record what they would send. Reading the real time inside a method forces the test to wait or to patch a global, and patching a shared clock affects every other user of it until it is restored.

Interview questions

Fresher · What is the difference between composition and inheritance, and when would you choose each?

Inheritance makes one class a special case of another: class Bus extends Vehicle gets every method of Vehicle through the prototype chain and can override them. Composition gives an object other objects to hold and call, such as a notifier that is given its senders and a clock. Inheritance suits a real “is a” relationship with a short chain whose subclasses keep every promise of the parent, such as Error subclasses. Composition suits behaviour that varies or combines: features become small parts that can be swapped, reused across classes and replaced by fakes in tests, whereas one subclass per combination of features multiplies quickly. In JavaScript, composition is also natural because any object with the right methods can stand in for another, without a shared base class.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A codebase has Notifier, SmsNotifier, RetryingSmsNotifier and QuietHoursRetryingSmsNotifier, and product now wants email and WhatsApp with the same retry and quiet-hours options. Propose a design that does not add a class per combination, show how you would migrate the existing callers safely, and explain how you would test it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Two teams publish mixins for your Order class, and both define save(): Auditable writes an audit entry and Cached clears a cache. With Object.assign only one save runs. With subclass factories, the order is sometimes not saved at all, because one team's save never calls super.save(). Explain both behaviours and design mixins that combine safely.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Inheritance shares behaviour by being; composition by having. Deep hierarchies multiply classes, tie subclasses to their parent’s internals and are hard to test.
  • A composed object holds small collaborators and calls them; wrappers add one feature to any collaborator, and passing collaborators in lets tests use fakes such as a fixed clock.
  • Duck typing is JavaScript’s polymorphism: any object with the methods a caller uses will do.
  • Object.assign mixins overwrite silently, freeze getters into values and copy nothing from class prototypes; copy descriptors if you use them.
  • Subclass-factory mixins build a real chain with working super, fields and private state; their order matters, they cannot be used with instanceof directly, and each application creates a new class.
  • Encapsulation, single responsibility, open/closed, substitutability and dependency inversion all apply, expressed with private fields, small objects, wrappers and injected collaborators.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.