Mixin and Typescript

这篇文章中翻译自 justinfagnani.com/2015/12/21/… 仅作分享,水平较渣,勿喷,欢迎指正。javascript

What is Mixin?

mixin 是一个抽象子类;即一个子类定义,能够应用于不一样的超(父)类以建立相关的修改类族群。java

​ —— Gilad Bracha 和 William Cook,基于 Mixin 的继承promise

上面是我能找到的 mixin 的最佳定义。它清楚地显示了 mixinnormal class 之间的区别,并强烈暗示了 mixin 如何在 JavaScript 中实现。markdown

为了更深刻地了解这个定义的含义,让咱们在 mixin 词典中添加三个术语:app

  • super class:在软件术语中,被继承的类通常称为超类,也有叫作父类。
  • mixin definition:能够应用于不一样超类(父类)的抽象子类的定义。
  • mixin application:将 mixin 定义应用于某个特定的超类,产生一个新的子类。

mixin defintion 其实是一个subclass factory,由超类来进行参数化,它生成 mixin applicationmixin application 位于子类和超类之间的继承层次结构中。ide

mixin 和普通子类之间惟一的区别在于普通子类有一个固定的超类(父类),而 mixin 定义的时候尚未超类。只有mixin application有本身的超类。能够将普通子类继承视为 mixin 继承的退化形式,其中超类在类定义时已知,而且只有一个 applicationsvg

Mixin applyment

javascript 中其实没有特别为 mixin 准备的语法,因此咱们拿 Dart 为例来看看 Mixin 的实际使用:函数

Simple Mixin

下面是 Dartmixins 的一个例子,它有一个很好的 mixins 语法,同时相似于 JavaScriptoop

class B extends A with M {}
复制代码

这里A是基类,B是子类,Mmixin definitionmixin application 是将 M 混合到 A 中的特定组合,一般称为A-with-MA-with-M 的超类是 A,而 B 的实际超类不是A,如您所料,而是A-with-Mui

让咱们从一个简单的类层级结构开始,B类继承自A类,Object 即根对象类:

class B extends A {}
复制代码

class-hierarchy-1-1.svg 如今让咱们添加 mixin

class B extends A with M {}
复制代码

class-hierarchy-2.svg

如您所见,mixin application *A-with-M*被插入到子类和超类之间的层次结构中。

注意:我使用长虚线表示 mixin defintion,使用短虚线表示 mixin application 的定义。

Multiple Mixins

Dart 中,多个 mixin 以从左到右的顺序应用,致使多个 mixin 应用程序被添加到继承层次结构中,这里咱们要知道 Mixin definition 上能够添加方法因此多层的 mixin 才有意义:

class-hierarchy-3.svg

class B extends A with M1, M2 {}
复制代码

Traditional JavaScript Mixins

JavaScript 中能够自由修改对象的能力意味着能够很容易地复制函数以实现代码重用,而无需依赖继承。

一般经过相似于如下的函数来实现:

function mixin(target, source) {
  for (var prop in source) {
    if (source.hasOwnProperty(prop)) {
      target[prop] = source[prop];
    }
  }
}
复制代码

它的一个版本甚至以 Object.assign 的形式出如今 JavaScript 中,因此咱们常常能在源码看到这样的写法:

const extend  = Object.assign;
const mixin  = Object.assign;
复制代码

咱们一般在原型上调用 mixin()

mixin(MyClass.prototype, MyMixin);
复制代码

如今,MyClass拥有了MyMixin中定义的全部属性。

若是你真的理解上面在 Dart 中的 Mixin 关系图,你必定会有一些疑惑,若是 MyClass.prototype 指的是 B,那 MyMixin 指的是 A 仍是 A with M。答案是这个不完备的实现方法中 MyMixin 指的是 Amixin(MyClass.prototype, MyMixin); 这个过程至关因而给 MyClass.prototype 添加 A with M,且 M 是无实体的。

这显然会带来不少的问题,下面来具体的探讨一下;

What's So Bad About That?

简单地将属性复制到目标对象中有一些问题。固然问题能够经过足够完备的 mixin 函数来解决:

1.Prototypes are modified in place.

当对原型对象使用 mixin 库时,原型会被直接改变。若是在任何其余不须要使用mixin来的属性的地方使用这个原型,那么就会出现问题。

2.super doesn't work.

既然JavaScript最终支持supermixin也应该支持。不幸的实际上咱们上面所实现的 mixin 直接对子类的 prototype 属性进行修改,并无建立实际的 A with M 的中间层,assign 来的属性不包括 __proto__ 因此在子类上调用 super 拿不到 A with M 也拿不到 A

3.Incorrect precedence(优先级).

虽然不必定老是这样,但正如示例中常常显示的那样,经过重写属性,mixin 来的方法优先于子类中的方法。而正确的思路是子类方法应该只优先于超类方法,容许子类覆盖 mixin 中的方法。

4.Composition is compromised(结构损坏)

Mixin 一般须要基础给原型链上的其余 mixin 或对象,可是上面的传统的 mixin 没有天然的方法来作到这一点。 由于属性是被函数被复制到对象上,简单的实现会覆盖现有的方法。而不是建立一个实际的mixin application 中间层。

同时对函数的引用在 mixin 的全部应用程序中都是重复的,在许多状况下,它们能够捆绑在引用相同原型中。通过覆盖属性,原型的结构和 JavaScript 的一些动态特性被减小:你不能轻易地内省 mixin 或删除或从新排序 mixin,由于 mixin 已直接扩展到目标对象中。

Better Mixins Through Class Expressions

了解了 mixin 这种模式的短处以后让咱们来看看改进版。让咱们快速列出咱们想要启用的功能,以便咱们能够根据它们来设计咱们的实现:

  • 根据上面的图,咱们知道其实 Mixin 应该是被添加到子类和超类之间的中间类,因此在 JavascriptMixin 应该被添加到原型链中。
  • Mixins application 不须要修改现有的对象。
  • 子类继承于 Mixins application 时不会修改子类。
  • super.foo 属性访问适用于 mixin 和子类。
  • Super()调用超类(A not A with M)构造函数。
  • Mixins 能够继承于其余 Mixins
  • instanceof 有效果。

SubClass Factory

上面我将 mixin 称为**“由超类进行参数化的子类工厂”**,在实际的实现中其实就是这样。

咱们依赖于JavaScript类的两个特性来实现这个子类工厂:

  1. 类能够用做表达式,也能够用做语句。做为表达式,它在每次求值时返回一个新类。

    let A = class {};
    let a = new A(); // A {}
    复制代码
  2. extends 操做接受返回类或构造函数的任意表达式

    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 */ }
    复制代码

定义mixin所须要的只是一个接受超类而后建立子类做为返回的函数,就像这样:

let MyMixin = (superclass) => class extends superclass {
  foo() {
    console.log('foo from MyMixin');
  }
};
复制代码

而后咱们能够像这样在 extends 子句中使用它:

class MyClass extends MyMixin(MyBaseClass) {
  /* ... */
}
复制代码

除了继承的方式还能够直接赋值生成没有子类属性和方法的 Mixin 类,这适用于只须要 Mixin DefinitionSuperClass 的交集的时候:

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]
复制代码

难以置信的简单,也难以置信的强大! 经过结合函数和类表达式,咱们获得了一个完备的 mixin 解决方案,它也能很好地泛化。咱们来看看这种实现方案下的原型链结构:

+---------------+
|               |
|  super Class  |
|		|
+---------------+
+---------------+ +---------------+
|  super Class  | |               |
|      with     | |    MyMixin	  |
|    MyMixin	| |               |
+---------------+ +---------------+
+---------------+
|               |
|    MyClass    |
|		|
+---------------+
复制代码

在这个原型结构中,MyMixin 做为工厂函数成为原型链的一环,而其经过 superClass 参数化的返回值 superClass with MyMixin 则做为 MyClasssuperClass 的中间层,拥有类实体。其自己经过 __proto__ 链接 superClass,而 MyClass 则经过 __proto__ 链接这个中间层。这和咱们预期的结构彻底一致。

如预期应用多个mixins工做:

class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
  /* ... */
}
复制代码

经过传递超类,mixin能够很容易地从其余mixin继承来:

let Mixin2 = (superclass) => class extends Mixin1(superclass) {
  /* Add or override methods here */
}
复制代码

Benefits of Subclass Factory

来看看这种方法实现的 mixin 的好处有哪些:

1.SubClass can override mixin methods.

正如我以前提到的,许多JavaScript mixin的例子都犯了这个错误,mixin会重写子类。经过咱们的方法,建立了中间层,子类正确地重写了重写超类方法的mixin方法而不是直接修改子类方法。

2.super works

这种实现中,super 在子类和 mixin 的方法中工做。 因为咱们永远不会覆盖类或 mixin 上的方法,所以它们可用于 super 寻址。

super 调用的好处对于那些不熟悉 mixin 的人来讲可能有点不直观,由于在 mixin definition 中不知道超类的存在,有时开发人员但愿 super 指向声明的超类(mixin 的参数),而不是 mixin application

3.Composition is preserved.

若是两个mixin能够定义相同的方法,而且只要每一层都调用super,它们都会被调用(即便覆盖super也能够调用父类方法)。有时,mixin 不知道超类是否具备特定的属性或方法,所以最好保护 super 调用。这么说可能不太清晰,来看看具体的效果:

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
复制代码

Constructor

构造函数是形成mixin混乱的一个潜在因素。它们本质上相似于方法,除了被覆盖的方法每每具备相同的签名,而继承层次结构中的构造函数一般具备不一样的签名。因为 mixin 不知道它可能被应用到哪一个超类,所以也不知道它的超类构造函数签名,所以调用super()可能很棘手。处理这个问题的最佳方法是始终将全部构造函数参数传递给super(),要么根本不在 superClass 定义构造函数,要么使用扩展操做符:super(…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');
复制代码

Mixin In Ts

上面的代码都是 Js 完成的,放到 ts 环境下会出现一点问题,好比咱们这个超类参数的类型如何书写:

let mixin = (superClass) =>
							// ^
							// Parameter 'superClass' implicitly has an 'any' type.
    class extends superClass {
        constructor(...args) {
            super(...args);
        }
    };
复制代码

还好TypeScript 2.2增长了对ECMAScript 2015 mixin类模式的支持,mixin超类构造类型指的是这样一种类型,它有一个构造签名,带有一个rest参数,类型为any[]和一个类对象返回类型。例如,给定一个类对象类型X, new(…args: any[]) => X是一个返回实例类型为 Xmixin超类构造函数类型。有了这个类型再对 mixin 函数作一些限制:

  • extends 表达式的类型参数类型必须限制为 mixin 超类构造函数类型。
  • mixin 类(若是有)的构造函数必须有一个 any[] 类型的其他参数,而且必须使用扩展运算符将这些参数做为参数传递给 super(...args) 调用。

一个 mixin 以后类表现为 mixin 超类构造函数类型(默认的)和参数基类构造函数类型之间的交集。

当获取包含mixin构造函数类型的交集类型的构造签名时,mixin 超类构造函数类型(默认的)被丢弃,其实例类型混合到交集类型中其余构造签名的返回类型中。 例如,交集类型 { new(...args: any[]) => A } & { new(s: string) => B } 具备单个构造签名 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;
复制代码

Mixin 类能够经过在类型参数的约束中指定构造签名返回类型来约束它们能够混合到的类的类型。 例如,如下 WithLocation 函数实现了一个子类工厂,该工厂将 getLocation 方法添加到知足 Point 接口的任何类(即具备类型为 numberxy 属性)。

interface Point {
  x: number;
  y: number;
}
const WithLocation = <T extends Constructor<Point>>(Base: T) =>
  class extends Base {
    getLocation(): [number, number] {
      return [this.x, this.y];
    }
  };
复制代码
相关文章
相关标签/搜索