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];
    }
  };
複製代碼
相關文章
相關標籤/搜索