This article translated from justinfagnani.com/2015/12/21/… Do only share, level more slag, do not spray, welcome correction.

What is Mixin?

Mixin is an abstract subclass; That is, a subclass definition that can be applied to different superclasses to create related modified class populations.

— Gilad Bracha and William Cook, inheritance based on mixins

Above is the best definition of a mixin I could find. It clearly shows the difference between mixins and normal classes and gives a strong indication of how mixins are implemented in JavaScript.

To get a deeper understanding of what this definition means, let’s add three terms to our mixin dictionary:

  • super class: In software terms,Be inheritedThe class ofThe super class, there are also called superclasses.
  • mixin definition: Definition of abstract subclasses that can be applied to different superclasses (superclasses).
  • mixin applicationWill:mixinThe definition applies to a particular superclass, producing a new subclass.

Mixin Defintion is actually a subclass Factory parameterized by the superclass, which generates mixin Application. Mixin Applications are located in the inheritance hierarchy between subclasses and superclasses.

The only difference between a mixin and a normal subclass is that a normal subclass has a fixed superclass (superclass), whereas a mixin does not have a superclass when it is defined. Only Mixin Applications have their own superclass. You can think of ordinary subclass inheritance as a degenerate form of mixin inheritance, where the superclass is known at class definition and there is only one application.

Mixin applyment

Javascript doesn’t have a syntax specifically for mixins, so let’s take a look at the actual use of mixins using Dart as an example:

Simple Mixin

Here’s an example of the Mixins in Dart that has a nice mixins syntax and is similar to JavaScript:

class B extends A with M {}
Copy the code

Where A is the base class, B is the subclass, and M is the mixin definition. A mixin application is A specific combination of M mixed into A, usually called A-with-M. The superclass of A-with-M is A, and the actual superclass of B is not A, as you might expect, but a-with-m.

Let’s start with A simple class hierarchy. Class B inherits from class A, and Object is the root Object class:

class B extends A {}
Copy the code

Now let’s addmixin:

class B extends A with M {}
Copy the code

As you can see, mixin Application * a-with-m * is inserted into the hierarchy between subclasses and superclasses.

Note: I use a long dotted line for mixin defintion and a short dotted line for Mixin Application definition.

Multiple Mixins

In Dart, multiple mixins are applied in order from left to right, resulting in multiple mixin applications being added to the inheritance hierarchy. Here we need to know that methods can be added to a mixin definition so that multiple layers of mixins make sense:

class B extends A with M1.M2 {}
Copy the code

Traditional JavaScript Mixins

The ability to freely modify objects in JavaScript means that functions can easily be copied for code reuse without relying on inheritance.

This is usually done by a function similar to the following:

function mixin(target, source) {
  for (var prop in source) {
    if(source.hasOwnProperty(prop)) { target[prop] = source[prop]; }}}Copy the code

A version of it even appears in JavaScript as object.assign, so we often see it written like this in source code:

const extend  = Object.assign;
const mixin  = Object.assign;
Copy the code

We usually call mixin() on a prototype:

mixin(MyClass.prototype, MyMixin);
Copy the code

MyClass now has all the properties defined in MyMixin.

If you really understand the Mixin diagram in Dart, you may have some questions about whether myclass. prototype refers to A or A with M, if myclass. prototype refers to B. The answer is that in this incomplete implementation MyMixin refers to A and mixin(myclass. prototype, MyMixin); This is equivalent to adding A with M to myclass. prototype, where M is non-entity.

This will obviously bring a lot of problems, the following specific discussion;

What’s So Bad About That?

There are some problems with simply copying properties into the target object. Of course the problem can be solved with sufficiently complete mixin functions:

1.Prototypes are modified in place.

When the mixin library is used on the prototype object, the prototype is changed directly. If you use this stereotype in any other place where you don’t need to use mixins for properties, you’ll have a problem.

2.super doesn’t work.

Since JavaScript finally supports Super, mixins should, too. Unfortunately, the mixin we implemented above directly modifies the prototype property of the subclass without creating the actual A with M middle layer. The assign attribute does not include __proto__, so calling super on A subclass does not get A with M.

3.Incorrect precedence.

This is not always the case, but as is often shown in examples, methods from mixins take precedence over methods in subclasses by overriding properties. The right idea is that subclass methods should only take precedence over superclass methods, allowing subclass methods to override methods in mixins.

4.Composition is compromised, structure compromised

Mixins often need to base themselves on other mixins or objects on the prototype chain, but traditional mixins above have no natural way to do this. Because properties are copied to objects by functions, a simple implementation overrides existing methods. Rather than creating an actual mixin Application middle tier.

Simultaneous references to functions are repeated across all applications of mixins, and in many cases they can be bundled with references to the same prototype. With overwritten properties, the structure of the prototype and some of the dynamic features of JavaScript are reduced: you can’t easily introspective mixins or remove or reorder mixins because mixins are directly extended to target objects.

Better Mixins Through Class Expressions

Now that we understand the drawbacks of the mixin model, let’s take a look at the improved version. Let’s quickly list the features we want to enable so that we can design our implementation according to them:

  • Based on the figure above, we know that in factMixinShould be added to the intermediate class between the subclass and the superclass, so inJavascriptMixinShould be added to the prototype chain.
  • Mixins applicationExisting objects do not need to be modified.
  • Subclasses inherit fromMixins applicationDoes not change subclasses when.
  • super.fooProperty access applies tomixinAnd the subclasses.
  • Super()Calling the superclass (A not A with M) constructor.
  • MixinsCan be inherited from othersMixins.
  • instanceofHave the effect.

SubClass Factory

Above I called mixins ** “subclass factories parameterized by superclasses” **, which is exactly what it is in the actual implementation.

We rely on two features of the JavaScript class to implement this subclass factory:

  1. Classes can be used as expressions or as statements. As an expression, it returns a new class each time it is evaluated.

    let A = class {};
    let a = new A(); // A {}
    Copy the code
  2. The extends operation accepts any expression that returns a class or constructor.

    class B extends function Foo(n) {this.n = n} { /* class B code */ }
    let b = new B(1); // B{}
    
    let rClass = (superClass) = > class extends superClass;
    class C extends rClass(B) { /* class C code */ }
    Copy the code

All you need to define a mixin is a function that takes a superclass and then creates a subclass as a return, like this:

let MyMixin = (superclass) = > class extends superclass {
  foo() {
    console.log('foo from MyMixin'); }};Copy the code

We can then use it in the extends clause like this:

class MyClass extends MyMixin(MyBaseClass) {
  / *... * /
}
Copy the code

In addition to inheritance, it is possible to generate a Mixin class with no subclass properties or methods by direct assignment. This works when only the intersection of a Mixin Definition and a SuperClass is required:

class Point {
    constructor(public x: number.public y: number){}}type Constructor<T> = new(... args:any[]) => T;
function Tagged<T extends Constructor<{}>>(Base: T) {
    return class extends Base {
        _tag: string;
        constructor(. args:any[]) {
            super(... args);this._tag = ' '; }}; }const TaggedPoint = Tagged(Point);
let point = new TaggedPoint(10.20);
point._tag = 'hospital';
// a hospital at [x: 10, y: 20]
Copy the code

Incredibly simple, and incredibly powerful! By combining functions and class expressions, we have a complete mixin solution that also generalizes well. Let’s look at the prototype chain structure under this implementation:

+---------------+
|               |
|  super Class  |
|		|
+---------------+
+---------------+ +---------------+
|  super Class  | |               |
|      with     | |    MyMixin	  |
|    MyMixin	| |               |
+---------------+ +---------------+
+---------------+
|               |
|    MyClass    |
|		|
+---------------+
Copy the code

In this prototype structure, MyMixin becomes a link in the prototype chain as a factory function, and its return value, superClass with MyMixin, parameterized by superClass, serves as a middle layer between MyClass and superClass and has class entities. Itself connects to superClass through __proto__, and MyClass connects to the middle layer through __proto__. This is exactly what we expect the structure to be.

Apply multiple mixins to work as expected:

class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
  / *... * /
}
Copy the code

Mixins can easily inherit from other mixins by passing superclasses:

let Mixin2 = (superclass) = > class extends Mixin1(superclass) {
  /* Add or override methods here */
}
Copy the code

Benefits of Subclass Factory

Let’s look at some of the benefits of this approach to mixins:

1.SubClass can override mixin methods.

As I mentioned earlier, many examples of JavaScript mixins make this mistake, and mixins override subclasses. With our method, creating the middle layer, the subclass correctly overwrites the mixin method that overwrites the superclass method instead of modifying the subclass method directly.

2.super works

In this implementation, super works in the methods of subclasses and mixins. Since we never override methods on classes or mixins, they are available for super addressing.

The benefits of calling super can be a little unintuitive to those unfamiliar with mixins, since superclasses are not known to exist in mixin definitions, and sometimes developers want super to point to the declared superclass (the argument to the mixin), Not Mixin Applications.

3.Composition is preserved.

If two mixins can define the same method, and as long as each layer calls super, they will be called (even if super is overridden, the superclass method can be called). Sometimes, mixins don’t know if a superclass has specific properties or methods, so it’s best to protect super calls. It may not be clear, but here’s how it works:

let Mixin1 = (superclass) = > class extends superclass {
  foo() {
    console.log('foo from Mixin1');
    if (super.foo) super.foo(); }};let Mixin2 = (superclass) = > class extends superclass {
  foo() {
    console.log('foo from Mixin2');
    if (super.foo) super.foo(); }};class S {
  foo() {
    console.log('foo from S'); }}class C extends Mixin1(Mixin2(S)) {
  foo() {
    console.log('foo from C');
    super.foo(); }}new C().foo();

// foo from C
// foo from Mixin1
// foo from Mixin2
// foo from S
Copy the code

Constructor

Constructors are a potential source of confusion in mixins. They are essentially similar to methods, except that the overridden methods tend to have the same signature, while constructors in inheritance hierarchies often have different signatures. Calling super() can be tricky because mixins don’t know which superclass it might be applied to, and therefore don’t know its superclass constructor signature. The best way to handle this is to always pass all constructor arguments to super(), either define no constructor in superClass at all, or use the extension operator :super(… The arguments).

let mixin = (superClass) = >
    class extends superClass {
        constructor(. args) {
            super(...args);
        }
    };

class GF {
    constructor(lastName) {
        this.lastName = lastName; }}class SON extends mixin(GF) {
    constructor(lastName) {
        super(lastName); }}let xiaoming = new SON('zhang');
Copy the code

Mixin In Ts

The above code is done in Js, and there are some problems in ts environment, such as how to write the type of the superclass parameter:

let mixin = (superClass) = >
							/ / ^
							// Parameter 'superClass' implicitly has an 'any' type.
    class extends superClass {
        constructor(. args) {
            super(...args);
        }
    };
Copy the code

Fortunately, TypeScript 2.2 adds support for ECMAScript 2015’s mixin class pattern. A mixin superclass construct type is a type that has a construct signature, a REST parameter, type any[], and a class object return type. For example, given an object of class type X, new(… Args: any[]) => X is a mixin superclass constructor type that returns instance type X. With this type in place, there are some restrictions on mixin functions:

  • extendsThe type parameter type of an expression must be limited tomixinSuperclass constructor type.
  • mixinThe constructor of the class (if any) must have oneany[]Type and must be passed as parameters using the extension operatorsuper(... args)The call.

A post-mixin class behaves as the intersection between the mixin superclass constructor type (the default) and the argument base class constructor type.

When the constructor signature of the intersection type containing the mixin constructor type is obtained, the mixin superclass constructor type (the default) is discarded and the example type is mixed into the return type of the other constructor signatures in the intersection type. For example, the intersection type {new(… Args: any[]) => A} & {new(s: string) => B} With A single construct signature new(s: string) => A & B.

class Point {
  constructor(public x: number.public y: number){}}class Person {
  constructor(public name: string){}}type Constructor<T> = new(... args:any[]) => T;

function TaggedMixin<T extends Constructor<{}>>(SuperClass: T) {
  return class extends SuperClass {
    _tag: string;
    constructor(. args:any[]) {
      super(... args);this._tag = ""; }}; }const TaggedPoint = TaggedMixin(Point);
let point = new TaggedPoint(10.20);
point._tag = "hello";

class Customer extends TaggedMixin(Person) {
  accountBalance: number;
}

let customer = new Customer("Joe");

customer._tag = "test";
customer.accountBalance = 0;
Copy the code

Mixin classes can constrain the types of classes to which they can be mixed by specifying the construct signature return type in the constraint of the type parameter. For example, the following WithLocation function implements a subclass factory that adds the getLocation method to any class that satisfies the Point interface (that is, has x and Y attributes of type number).

interface Point {
  x: number;
  y: number;
}
constWithLocation = <T extends Constructor<Point>>(Base: T) => class extends Base { getLocation(): [number, number] { return [this.x, this.y]; }};Copy the code