completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Mixins & composition

Share behavior across unrelated JavaScript classes with mixin objects, class mixin functions, trait-style conflict checks, composition, and a small event mixin.

By the end, you can
  • 01
    Copy a mixin object safelyUse Object.assign to copy methods into a prototype, and spot when it replaces a method or turns a getter into a plain value.
  • 02
    Wrap class mixin functionsRead the chain of classes they make, and guess the order of super calls.
  • 03
    Choose composition when it is simplerCompare mixins, traits, extends, and objects built from parts, and pick the one that fits.

Share one ability with classes that aren't related

A class can extend another class with extends. That works well when one thing really is a special kind of another. For example, an AdminUser is a kind of User.

Sometimes, though, very different classes need the same small skill. Say a User, an Order, and an Invoice all need a toJSON() method. That method turns the object into plain data you can save or send. But an order is not a kind of user, and an invoice is not a kind of order. So extends doesn't fit.

A mixin solves this. A mixin is one small ability, such as toJSON(), that you add to a class or an object. You add only the ability the class needs. You don't make every class extend one big parent class.

Real-life analogyA mixin is a sticker pack of abilities

Picture a sheet of stickers. Each sticker names one thing an object can do: "can save", "can check itself", "can send messages". You can put the same sticker on a laptop, a bottle, and a notebook. The sticker doesn't turn the bottle into a laptop. It only adds one ability.

In real life: A waterproof sticker
In JavaScript: A CanSerialize mixin with toJSON()
In real life: Put it on a laptop, bottle, or notebook
In JavaScript: Apply it to User, Order, or Invoice
In real life: Two stickers with the same label cover each other
In JavaScript: Two methods with the same name clash

There is a second way to share abilities, called composition. With composition, you build an object out of smaller parts, and the object uses those parts. You don't ask which parent class it should extend.

Real-life analogyComposition builds from parts, not ancestors

A desk is made from legs, a top, and drawers. If you want wooden legs instead of metal ones, you swap just the legs. Nobody asks who the desk's parents are.

In real life: A desk built from legs, a top, and drawers
In JavaScript: An object built from parts, like its data and an event hub
In real life: Swap metal legs for wooden legs
In JavaScript: Pass in a different part, for example in a test
In real life: Nobody asks about the desk's parents
In JavaScript: No extends is needed

This lesson shows five ways to share an ability, in this order:

  1. Mixin objects: copy methods from a plain object.
  2. Class mixin functions: a function that wraps a class in a new class.
  3. Traits: mixins that stop with an error when two names clash.
  4. Composition: build an object from parts.
  5. An event mixin: a real, useful ability with on, off, and emit.

It builds on the earlier Delegation vs classical inheritance lesson. Events get their own lessons later (Observer & pub/sub and Build an event emitter). Here they are just one example of an ability you can share.

Mixin objects: copy methods onto a prototype

STEP THROUGH

The simplest mixin is a mixin object: a plain object that holds a few methods. You copy those methods into a class's prototype with Object.assign(target, source). Object.assign copies the properties of source into target.

Why the prototype? Every class has one object called its prototype, such as User.prototype. When a user object doesn't have a method itself, JavaScript looks for it there. So a method copied into User.prototype can be used by every user, old and new.

Object.assign(User.prototype, Serializable). A box called Serializable holds a toJSON function. An arrow labeled copy points to a cart called prototype, which now holds describe and toJSON.Object.assign(User.prototype, Serializable)toJSON()Serializablemixin objectcopydescribe()toJSON()prototype
Object.assign copies toJSON() from the mixin object into User.prototype. Now every User can call it, next to its own describe().

Object.assign doesn't copy everything. It copies only the source's own enumerable properties:

  • Own means the property sits on the source object itself. A property the source only finds on its own prototype is not own.
  • Enumerable means the property shows up when you list the object's keys. Most properties are enumerable. You can hide one with Object.defineProperty.
Only own, listed properties are copiedPop out in the code editor (opens in a new tab)JavaScript
const inherited = { inherited() { return "not copied"; } };
const source = Object.create(inherited);
source.own = "copied";
Object.defineProperty(source, "hidden", { value: "not copied", enumerable: false });
const target = {};
Object.assign(target, source);
console.log(target.own);
console.log(typeof target.inherited);
console.log(target.hidden);
  1. Line 1 makes an object with an inherited() method.
  2. Line 2 makes source, with that object as its prototype. So source can use inherited(), but the method is not its own.
  3. Line 3 gives source its own property, own.
  4. Line 4 adds a property called hidden and sets enumerable: false, so it won't be listed.
  5. Lines 5 and 6 make an empty target and copy source into it.
  6. Line 7 prints copied. Lines 8 and 9 print undefined: the inherited method and the hidden property were skipped.

So put a mixin's methods directly on the mixin object. Don't leave them on its prototype, or they won't be copied.

Object.assign can copy several sources in one call. What if two of them have a method with the same name?

The last copy winsPop out in the code editor (opens in a new tab)JavaScript
const First = { describe() { return "first"; } };
const Second = { describe() { return "second"; } };
const card = {};
Object.assign(card, First, Second);
console.log(card.describe());

Lines 1 and 2 make two mixin objects. Both have a method called describe. Line 3 makes an empty card. Line 4 copies First, then Second, into the card. Line 5 prints second. The second describe took the place of the first one, and JavaScript gave no warning.

Object.assign(card, First, Second). After First is copied, card.describe returns first. Then Second is copied over it, and card.describe returns second.Object.assign(card, First, Second)after Firstdescribe()cardreturns "first"copy Secondafter Seconddescribe()cardreturns "second"
Both mixin objects have a method called describe. Second is copied last, so its describe takes the place of the first one, with no warning.

The next example puts these ideas together. A User class gets three mixin objects. One of them has a getter. A getter is a property that runs a function each time you read it, written as get name() { ... }. Step through and watch what happens to the getter and to describe.

Copy three mixin objects into a class
Step 0 of 11Ready
Your turn: follow the blue line

Guess which describe wins, then step through the copies. Each step shows what Object.assign really did, including the replaced method and the getter that became a plain value.

Running in
  1. script
Next: line 1
  toJSON() { return { type: this.type, id: this.id }; }};const Labels = {  describe() { return "mixin label for " + this.id; }};const GetterPack = {  get createdLabel() { return "copied once"; }};class User {  constructor(id) { this.type = "user"; this.id = id; }  describe() { return "user " + this.id; }}Object.assign(User.prototype, Serializable);Object.assign(User.prototype, Labels);Object.assign(User.prototype, GetterPack);const ada = new User("ada");console.log(ada.describe());console.log(JSON.stringify(ada.toJSON()));console.log(typeof Object.getOwnPropertyDescriptor(User.prototype, "createdLabel").get);console.log(ada.createdLabel);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Two mistakes to watch for
  • A copy can replace a method with no warning. If the prototype already has describe and the mixin also has describe, the one copied last wins.
  • Object.assign turns a getter into a plain value. It reads the getter while copying, so the getter runs once. Only the value it returns is copied. To keep the getter, use Object.getOwnPropertyDescriptors with Object.defineProperties, as shown next.

Every property has a descriptor: a small object that describes it. For a getter, the descriptor holds the getter function under the name get. Copy the descriptor, and you copy the getter itself, not its value.

Copy a getter so it stays a getterPop out in the code editor (opens in a new tab)JavaScript
const source = {
  get label() { return "fresh"; }
};
const target = {};
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
console.log(Object.getOwnPropertyDescriptor(target, "label").get !== undefined);
console.log(target.label);
  1. Lines 1 to 3 make source with a getter called label that returns "fresh".
  2. Line 4 makes an empty target.
  3. Line 5 reads every descriptor of source and puts them on target.
  4. Line 6 prints true: the copy still has a get function.
  5. Line 7 reads target.label. The getter runs and gives fresh.

Copying descriptors keeps getters working. But it still doesn't check for two sources with the same name. The traits section below adds that check.

Mixin objects work best for small methods that stand on their own. A copied method can't use super to call the parent class's version of a method. When you need super, use a class mixin function instead.

Class mixin functions: a function that returns a class

INTERACTIVE

A class mixin function is a function that takes a class and returns a new class. The new class extends the one you passed in and adds one ability. Because the result is a real class, its methods can use super to call the parent class's version of a method.

The shape of a class mixin functionPop out in the code editor (opens in a new tab)JavaScript
const Serializable = (Base) => class Serializable extends Base {
  toJSON() { return { ...this }; }
};

class Order extends Serializable(BaseRecord) {}
  1. Line 1 makes Serializable, an arrow function. It takes a class called Base and returns a new class that extends Base.
  2. Line 2 is the ability the new class adds: a toJSON() method.
  3. Line 5 calls Serializable(BaseRecord). That call returns a class, and Order extends it.

Here is a complete version you can run:

A class mixin function returns a real classPop out in the code editor (opens in a new tab)JavaScript
class BaseRecord {
  constructor(id) { this.id = id; }
}
const Serializable = (Base) => class extends Base {
  toJSON() { return { id: this.id }; }
};
class Order extends Serializable(BaseRecord) {}
const order = new Order("A17");
console.log(JSON.stringify(order.toJSON()));
console.log(order instanceof BaseRecord);
  1. Lines 1 to 3 make BaseRecord. Its constructor stores an id.
  2. Lines 4 to 6 make the class mixin function. The class it returns adds toJSON().
  3. Line 7 makes Order extend the class that Serializable(BaseRecord) returns.
  4. Line 8 makes an order. The BaseRecord constructor still runs, so the order gets id "A17".
  5. Line 9 prints {"id":"A17"}, using the added toJSON().
  6. Line 10 prints true. The order is still a BaseRecord, because the new class extends it.

You can wrap one mixin inside another, like Serializable(Auditable(BaseRecord)). The innermost call runs first. So the chain of classes is:

  • Order extends Serializable,
  • which extends Auditable,
  • which extends BaseRecord.

When you call a method, JavaScript looks in the nearest class first. If that method calls super.describe(), the call goes one class further down. Each class can add its own text when the answer comes back.

class Order extends Serializable(Auditable(BaseRecord)). Four cards in a row: Order, Serializable, Auditable, BaseRecord, joined by arrows. BaseRecord gives record A17, Auditable adds audit, Serializable adds serial. The result is record A17 + audit + serial.class Order extends Serializable(Auditable(BaseRecord))OrderSerializableAuditableBaseRecordstarts here+ serial+ auditrecord A17record A17 + audit + serial
order.describe() starts at Order and each super.describe() goes one class further right. BaseRecord answers first, then each class adds its text on the way back.

Step through the full example below. Then switch the wrapping order on line 14 and step through again.

Wrap two class mixins around one class
Step 0 of 10Ready
Your turn: follow the blue line

Pick a wrapping order, guess the chain of classes, then step through and check.

Running in
  1. script
Next: line 1
  constructor(id) { this.id = id; }  label() { return "record " + this.id; }  describe() { return this.label(); }}const Auditable = (Base) => class Auditable extends Base {  describe() { return super.describe() + " + audit"; }  audit() { return "checked " + this.id; }};const Serializable = (Base) => class Serializable extends Base {  describe() { return super.describe() + " + serial"; }  toJSON() { return { id: this.id, audit: this.audit() }; }};class Order extends Serializable(Auditable(BaseRecord)) {}const order = new Order("A17");console.log(order.describe());console.log(JSON.stringify(order.toJSON()));console.log(Object.getPrototypeOf(Order.prototype).constructor.name);console.log(Object.getPrototypeOf(Object.getPrototypeOf(Order.prototype)).constructor.name);console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Order.prototype))).constructor.name);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick a wrapping order for line 14, then guess the chain of classes.

Both orders work. JavaScript looks in the outer wrapper first, and each super.describe() goes one class further down.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The last three lines of the example print the class names in the chain. Object.getPrototypeOf(Order.prototype) gives the prototype of the class that Order extends. Its constructor.name is that class's name. When you change the wrapping order, the chain changes, and so does the text from describe().

When a class mixin is the right tool
  • Use a class mixin for a small ability that works on the object and may need super.
  • Use a plain function when you only turn one value into another, such as formatPrice(price).
  • Use composition when the ability needs its own settings, its own data, or should be easy to swap.

This keeps each mixin small, so it doesn't grow into a second big parent class.

Traits: mixins that refuse to overwrite

GUARDED COPY

You saw that Object.assign replaces a method with the same name and gives no warning. A trait is a mixin with a rule against that: if a name is already taken, stop with an error.

JavaScript has no trait keyword. You write the rule yourself in a small helper function, like this one:

A small trait helperPop out in the code editor (opens in a new tab)JavaScript
function applyTraits(target, ...traits) {  for (const trait of traits) {    for (const key of Reflect.ownKeys(trait)) {      if (key in target) throw new Error("Trait conflict: " + String(key));      Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(trait, key));    }  }}const CanTag = { tag() { return "tag:" + this.id; } };const CanLabel = { label() { return "label:" + this.id; } };class Photo { constructor(id) { this.id = id; } }applyTraits(Photo.prototype, CanTag, CanLabel);console.log(new Photo("p1").tag());applyTraits(Photo.prototype, { tag() { return "again"; } });
  1. Lines 1 to 3: applyTraits goes through every trait, and every key in each trait. Reflect.ownKeys lists all of an object's own keys.
  2. Line 4 is the rule. key in target asks "does the target already have this name?" If yes, it throws an error.
  3. Line 5 copies the property's descriptor, so a getter stays a getter.
  4. Lines 9 to 12 make two traits and a Photo class, then add both traits to Photo.prototype.
  5. Line 13 prints tag:p1.
  6. Line 14 tries to add a second tag. The prototype already has one, so the helper throws Trait conflict: tag.
A second tag function tries to go into Photo.prototype, which already holds tag and label. The arrow stops short, and the helper throws Trait conflict: tag.second tag()tag()Trait conflict: tagPhoto.prototypetag()label()prototype
Photo.prototype already has tag, so applyTraits throws before it copies anything. The clash shows up as an error instead of a silent swap.

A bigger trait helper could also let you rename a method, leave one out, or pick which one wins. Either way, a clash becomes a choice you make, not something that happens by accident.

The helper keeps getters

The helper copies with Object.getOwnPropertyDescriptor and Object.defineProperty. That copies each property's descriptor, so getters stay getters. The helper could also read other settings in the descriptor, such as enumerable and writable (whether the value can be changed).

Composition over inheritance: build from parts

COMPARE

You may hear the advice "composition over inheritance". It doesn't mean "never use extends". It means: build objects from small parts first. Use extends only when one thing clearly is a kind of another, and that won't change.

A factory function is a plain function that makes and returns an object. Here, two factory functions each make one part. A third one, createOrder, puts the parts together.

An order built from partsPop out in the code editor (opens in a new tab)JavaScript
function createSerializable(model) {  return { toJSON() { return { ...model.data }; } };}function createEventHub() {  const listeners = new Set();  return {    on(listener) { listeners.add(listener); },    emit(value) { for (const listener of listeners) listener(value); }  };}function createOrder(id) {  const model = { data: { id, status: "new" } };  return { ...model, ...createSerializable(model), events: createEventHub() };}const order = createOrder("A17");order.events.on((status) => { order.data.status = status; });order.events.emit("paid");console.log(JSON.stringify(order.toJSON()));
  1. Lines 1 to 3: createSerializable makes a part with a toJSON() method. That method returns a copy of the order's data.
  2. Lines 4 to 10: createEventHub makes a part that keeps a list of listener functions. on adds a listener, and emit calls every listener.
  3. Line 12 makes the order's data: an id and a status of "new".
  4. Line 13 builds the order from the data, the toJSON() part, and an event hub called events.
  5. Line 16 adds a listener that changes the order's status. Line 17 calls it with "paid".
  6. Line 18 prints {"id":"A17","status":"paid"}.

No class had to extend a parent class with every ability inside it. In a test, you could pass in a different event hub and leave the rest alone.

The table below compares the four ways to share an ability. Read each row as: "this pattern adds this, works best here, and needs care here."

Four ways to share an ability
PatternWhat it addsWorks best whenTake care that
Mixin objectIt copies methods from a plain object into a prototype.Classes that aren't related need the same small ability.A copy replaces a method with the same name, with no warning. Object.assign turns getters into plain values.
Class mixin functionIt returns a new class that extends any class you give it.The ability needs super, or you want to wrap several abilities around one class.The wrapping order decides which method JavaScript finds first.
TraitIt is a mixin that throws an error when a name is already taken.You want to copy methods, but never replace one by accident.JavaScript has no traits built in, so you write the rule yourself.
CompositionIt builds an object from parts the object keeps and uses.A part keeps its own data, or should be easy to swap.There is no super. You call each part yourself.

Now try choosing. For each situation, pick the pattern that fits best.

Mixin, inheritance, or composition?
  • Users and Orders both need a toJSON() method, but neither is a kind of the other.
  • AdminUser really is a special kind of User and should reuse the User constructor.
  • An Order needs logging, checking, and a payment service that a test can swap out.
  • Cards, dialogs, and images all need a small enableDrag() ability.
  • A custom list should work like an Array and pass Array.isArray checks.
  • A dashboard keeps a notifier, a store, and a renderer, and each one can be replaced.
Try it yourself
0 of 6 correct

Pick the pattern that fits each situation. Each answer explains why.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

An event mixin: on, off, emit

LIVE PLAYGROUND

An event is a named moment, such as "done" when a download finishes. A listener is a function that should run when that event happens. Many different classes need events, so an event mixin is a useful ability to share. It has three methods:

  • on(name, listener) saves a listener for an event name. It returns a function that removes the listener again.
  • off(name, listener) removes a saved listener.
  • emit(name, payload) calls every listener saved for that name, and passes them the payload value.

The methods are copied into the prototype, so every download shares them. But the listeners are saved in this._events, on each object itself. So each download keeps its own list of listeners.

Use an event mixin
The event mixin, added to a Download classPop out in the code editor (opens in a new tab)JavaScript
const EventMixin = {  on(name, listener) {    this._events ??= new Map();    const listeners = this._events.get(name) ?? new Set();    listeners.add(listener);    this._events.set(name, listeners);    return () => this.off(name, listener);  },  off(name, listener) {    this._events?.get(name)?.delete(listener);  },  emit(name, payload) {    for (const listener of this._events?.get(name) ?? []) listener(payload);  }};class Download { constructor(file) { this.file = file; } }Object.assign(Download.prototype, EventMixin);const download = new Download("mixins.pdf");const unsubscribe = download.on("done", (file) => console.log("saved " + file));download.emit("done", download.file);unsubscribe();download.emit("done", "nothing logs");
What happened0 saved
instancedownload.file = "mixins.pdf"
eventdone
listeners0
  1. No calls yet.
Try it yourself

No listeners are saved yet. Click a Subscribe button, then click Emit done.

The buttons call the same on, off, and emit methods shown in the code. Reset makes a new download with no listeners.

The buttons call the real on, off, and emit methods from the code. Try this:

  1. Subscribe both listeners.
  2. Click Emit done. Both listeners run.
  3. Unsubscribe one listener.
  4. Emit again. Only the other listener runs.
A small example on purpose

This is enough to learn mixins. Later, Observer & pub/sub explains how to plan events in a bigger program, and Build an event emitter turns this small mixin into a complete tool.

Where you will use this

Mixins help when very different classes need the same small skill. Common examples:

  • Turning objects into plain data with toJSON().
  • Checking that values are valid.
  • Sending events, or writing log messages.
  • Letting cards, dialogs, and images be dragged.
  • Checking whether a user is allowed to do something.

Pick the smallest tool that does the job:

  • Use a mixin object for two or three methods that keep no data of their own and can be copied as they are.
  • Use a class mixin function when the ability needs super, or when you want to wrap several abilities around one class.
  • Use a trait helper when replacing a method by accident would cause bugs, so a clash should throw an error.
  • Use composition when the ability keeps its own data or should be easy to swap.

To review the background, see Delegation vs classical inheritance and Objects & references.

Common misconceptions

"Mixins are just inheritance with a different name."

A class mixin function does make new classes with extends. But the goal is different. A mixin adds one small ability to classes that aren't related. It doesn't put every class under one parent.

"Object.assign is always a safe way to add a mixin."

It is quick, not safe. It replaces methods with the same name and gives no warning. It also turns getters into plain values.

"Traits are built into JavaScript."

JavaScript has no trait keyword. You write the trait rules yourself, in a helper function that copies descriptors.

"Composition means no classes."

Composition means building from parts. A part can be a plain object, a function, an object made from a class, or a class made by a mixin.

"An event mixin is a full event system."

The small mixin here shows shared methods that keep data on each object. Real event systems add more: listeners that run only once, error handling, listeners that wait for slow work, a fixed calling order, and cleanup rules.

Practice: mixins and composition

5 EXERCISES

Guess the output of each program before you run it. Use the hints if you get stuck.

Exercise 1 · Warm-upAdd a mixin object to a class

A ticket class gets an archive() method from a mixin object. Run the code and type the exact line it prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const CanArchive = {
  archive() { return this.id + ":archived"; }
};
class Ticket { constructor(id) { this.id = id; } }
Object.assign(Ticket.prototype, CanArchive);
console.log(new Ticket("T1").archive());

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeGuess the order of super calls

    Two class mixins are wrapped around Base. Without running it first, guess the printed text.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    class Base { name() { return "Base"; } }
    const A = (BaseClass) => class A extends BaseClass {
      name() { return super.name() + "->A"; }
    };
    const B = (BaseClass) => class B extends BaseClass {
      name() { return super.name() + "->B"; }
    };
    class Final extends B(A(Base)) {}
    console.log(new Final().name());

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCatch a name clash

      This helper catches the clash instead of replacing the method. What message does it print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function safeAssign(target, source) {
        for (const key of Reflect.ownKeys(source)) {
          if (key in target) throw new Error("Conflict: " + String(key));
          Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
        }
      }
      const CanName = { name() { return "mixin"; } };
      class Person { name() { return "person"; } }
      try { safeAssign(Person.prototype, CanName); } catch (error) { console.log(error.message); }

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWrite a class mixin function

        Write a class mixin function that adds a label() method. What label does it print?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        class Base { constructor(name) { this.name = name; } }
        const Named = (BaseClass) => class Named extends BaseClass {
          label() { return "item:" + this.name; }
        };
        class Product extends Named(Base) {}
        console.log(new Product("book").label());

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeAdd an event mixin to a class

          Finish the event mixin, add it to Timer, and check what it logs.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const EventMixin = {
            on(name, listener) { this.events ??= {}; (this.events[name] ??= []).push(listener); },
            emit(name, value) { for (const listener of this.events?.[name] ?? []) listener(value); }
          };
          class Timer {}
          Object.assign(Timer.prototype, EventMixin);
          const timer = new Timer();
          timer.on("tick", (value) => console.log("tick " + value));
          timer.emit("tick", 1);

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            7 QUESTIONS

            For each code question, ask: what was copied, and in what order? Did a name clash? Was a getter read? For class mixins, write down the chain of classes first. Each answer explains why it's right or wrong.

            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What is a mixin object?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const M = { speak() { return "mixin"; } };
              class Pet { speak() { return "pet"; } }
              Object.assign(Pet.prototype, M);
              console.log(new Pet().speak());

              Choose an answer to see the explanation.

            3. Question 3 of 7What happens to a getter copied with Object.assign?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does this code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              class Base { name() { return "Base"; } }
              const A = (BaseClass) => class A extends BaseClass { name() { return super.name() + " A"; } };
              const B = (BaseClass) => class B extends BaseClass { name() { return super.name() + " B"; } };
              class Final extends B(A(Base)) {}
              console.log(new Final().name());

              Choose an answer to see the explanation.

            5. Question 5 of 7Why use a trait helper instead of plain Object.assign?

              Choose an answer to see the explanation.

            6. Question 6 of 7Which one is composition?

              Choose an answer to see the explanation.

            7. Question 7 of 7What does this event mixin code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const EventMixin = {
                on(name, listener) { this.events ??= {}; (this.events[name] ??= []).push(listener); },
                emit(name, value) { for (const listener of this.events?.[name] ?? []) listener(value); }
              };
              class Job {}
              Object.assign(Job.prototype, EventMixin);
              const job = new Job();
              job.on("done", (value) => console.log("done " + value));
              job.emit("done", "A");

              Choose an answer to see the explanation.

            Key takeaways

            • A mixin is one small ability that you add to classes that aren't related.
            • A mixin object is a plain object of methods. Object.assign copies them into a prototype.
            • Object.assign copies only own, listed properties. It replaces methods with the same name with no warning, and it turns getters into plain values.
            • A class mixin function takes a class and returns a new class that extends it. Its methods can use super, and you can wrap several around one class.
            • A trait is a mixin that throws an error when a name is already taken. You write that rule yourself.
            • Composition builds an object from small parts. It is often simpler than a long chain of parent classes.
            • An event mixin shares on, off, and emit with any class.

            Remember the one-liner.
            A mixin adds one small ability to a class or object. Composition builds an object from small parts instead of parent classes.

            Up next: OOP principles in JavaScript.

            CompleteFrontend Clear concepts. Working examples.