Advanced Composition and Metadata

Introduction: Beyond Traditional Inheritance

In our previous lessons, we explored how to build robust classes and how to share logic through inheritance. We saw how a SavingsAccount could inherit from a base Account to gain its features. While inheritance is powerful, it is also very rigid. It creates a strict parent-child relationship that can sometimes make your code difficult to change. If you have many different behaviors — like logging, auditing, or tagging — trying to fit them all into a single inheritance tree often leads to "messy" code where classes do too much.

In this lesson, we will look at two modern patterns that help us move beyond simple inheritance. First, we will learn how to use a WeakMap to store data about an object without actually modifying the object itself. This is perfect for tasks like creating an audit log that stays separate from the object's public properties. Second, we will learn about mixins. Mixins allow us to "plug in" specific behaviors to any class we want, giving us the flexibility to combine multiple features horizontally rather than vertically.

Understanding WeakMap

JavaScript provides a special kind of collection called a WeakMap. At first glance, it looks like a regular Map, but it has two very important differences. The first difference is that the keys in a WeakMap must be objects. You cannot use a string or a number as a key. The second, and most important, difference involves garbage collection. In a standard Map, if you use an object as a key, that object will stay in the computer's memory as long as the Map exists. This can sometimes lead to memory leaks, where your program uses more and more RAM over time.

A WeakMap solves this by holding "weak" references to its keys. When no strong references to the key object remain, the WeakMap entry can be garbage-collected automatically. This makes it an ideal tool for storing extra information about an object without worrying about cleaning up the memory later.

However, WeakMap has specific limitations. Unlike a standard Map, it cannot be iterated and does not expose a size property; you must already have the key object to retrieve its metadata. Generally, you should use a Map when enumeration is required, and a WeakMap when object-keyed metadata and garbage-collection friendliness matter. Note that any "privacy" provided by a WeakMap is scope-based: metadata is hidden from the object instance, but remains accessible to any code that has a reference to the WeakMap variable itself.

Storing External Metadata with WeakMap

One of the best uses for a WeakMap is storing metadata, which is simply data about data. Imagine you have an Account class, but you want to keep a history of every event that happens to an account instance. You could add an auditLog array inside the class, but that might clutter your business logic. By using a WeakMap, you can store this history externally. This ensures the data does not appear on the account object itself; it remains private as long as the WeakMap is not exposed.

"use strict";

const audit = new WeakMap();

function logEvent(obj, event) {
  const entries = audit.get(obj) ?? [];
  entries.push({ event, at: Date.now() });
  audit.set(obj, entries);
}

class Account {
  constructor(owner) { this.owner = owner; }
}

const a = new Account("Ada");
logEvent(a, "created");
logEvent(a, "deposit:50");

console.log("audit log:", audit.get(a));

In the code above, we define a constant called audit which is our WeakMap. The logEvent function takes an object and a message. It checks if the object already has a list of entries in the WeakMap using the get method. If it doesn't, it starts with an empty array using the nullish coalescing operator (??). After adding the new event, it saves the list back into the WeakMap using set. This keeps the Account class clean while allowing us to track exactly what happens to each instance.

Output:

audit log: [
  { event: 'created', at: 1715832000000 },
  { event: 'deposit:50', at: 1715832000005 }
]

The Limits of Single Inheritance

In JavaScript, a class can only have one parent. This is known as single inheritance. You use the extends keyword to link one class to another. This works well if you have a clear hierarchy, like a Dog inheriting from Animal. However, what happens if you want an Account to have logging, tagging, and security features?

If you try to use inheritance for all of these, you end up with a very long chain of classes. This makes your code fragile because a small change at the top of the chain can break everything below it. Furthermore, it is difficult to give those same logging features to a completely different class, like a User or a Transaction, without repeating your code. To solve this, we need a way to share behavior horizontally across many different classes.

Mixins: Functions That Add Behavior

Composing Multiple Mixins

The true power of mixins appears when we combine them. Because each mixin is just a function that returns a class, we can wrap them around each other. This is called composition. We can take our original Account class and pass it through both Taggable and Loggable to create a new class that has all the features of all three.

class LoggedTaggedAccount extends Loggable(Taggable(Account)) {}

const lt = new LoggedTaggedAccount("Grace").addTag("vip");
lt.log("welcome");
console.log("tags:", lt.tags);

When we define LoggedTaggedAccount, we call Taggable(Account) first, which creates an account that can be tagged. Then, we pass that result into Loggable, which adds the logging ability. The final lt instance has the owner property from the original Account, the tags logic from the Taggable mixin, and the log method from the Loggable mixin. This allows you to build complex objects by picking and choosing exactly which features they need, rather than being stuck in a rigid inheritance tree.

Output:

[LoggedTaggedAccount] welcome
tags: [ 'vip' ]

Summary and Practice Preview

In this lesson, we moved beyond basic class structures to explore advanced patterns for modern JavaScript development. We learned how to use a WeakMap to store external metadata about objects, ensuring our code stays clean and memory-efficient while maintaining scope-based privacy. We also explored the limitations of single inheritance and how mixins provide a flexible alternative. By using functions to wrap classes, we can compose multiple behaviors together, creating highly reusable and modular code.

These patterns are used frequently in large-scale applications where flexibility and clean data separation are critical. In the upcoming practice exercises, you will have the chance to implement your own WeakMap logic and create custom mixins to enhance your classes. All the tools are ready for you in the CodeSignal IDE, so you can jump straight into the code. Happy practicing!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal