這篇文章中翻譯自 justinfagnani.com/2015/12/21/… 僅作分享,水平較渣,勿噴,歡迎指正。javascript
mixin
是一個抽象子類;即一個子類定義,能夠應用於不一樣的超(父)類以建立相關的修改類族羣。java —— Gilad Bracha 和 William Cook,基於 Mixin 的繼承promise
上面是我能找到的 mixin
的最佳定義。它清楚地顯示了 mixin
和 normal class
之間的區別,並強烈暗示了 mixin
如何在 JavaScript
中實現。markdown
爲了更深刻地瞭解這個定義的含義,讓咱們在 mixin
詞典中添加三個術語:app
super class
:在軟件術語中,被繼承的類通常稱爲超類,也有叫作父類。mixin definition
:能夠應用於不一樣超類(父類)的抽象子類的定義。mixin application
:將 mixin
定義應用於某個特定的超類,產生一個新的子類。mixin defintion
其實是一個subclass factory
,由超類來進行參數化,它生成 mixin application
。mixin application
位於子類和超類之間的繼承層次結構中。ide
mixin
和普通子類之間惟一的區別在於普通子類有一個固定的超類(父類),而 mixin
定義的時候尚未超類。只有mixin application
有本身的超類。能夠將普通子類繼承視爲 mixin
繼承的退化形式,其中超類在類定義時已知,而且只有一個 application
。svg
javascript
中其實沒有特別爲 mixin
準備的語法,因此咱們拿 Dart
爲例來看看 Mixin
的實際使用:函數
下面是 Dart
中 mixins
的一個例子,它有一個很好的 mixins
語法,同時相似於 JavaScript
:oop
class B extends A with M {}
複製代碼
這裏A
是基類,B
是子類,M
是mixin definition
。mixin application
是將 M
混合到 A
中的特定組合,一般稱爲A-with-M
。A-with-M
的超類是 A
,而 B
的實際超類不是A
,如您所料,而是A-with-M
。ui
讓咱們從一個簡單的類層級結構開始,B
類繼承自A
類,Object
即根對象類:
class B extends A {}
複製代碼
如今讓咱們添加 mixin
:
class B extends A with M {}
複製代碼
如您所見,mixin application *A-with-M*
被插入到子類和超類之間的層次結構中。
注意:我使用長虛線表示
mixin defintion
,使用短虛線表示mixin application
的定義。
在 Dart
中,多個 mixin
以從左到右的順序應用,致使多個 mixin
應用程序被添加到繼承層次結構中,這裏咱們要知道 Mixin definition
上能夠添加方法因此多層的 mixin
纔有意義:
class B extends A with M1, M2 {}
複製代碼
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
指的是 A
而 mixin(MyClass.prototype, MyMixin);
這個過程至關因而給 MyClass.prototype
添加 A with M
,且 M
是無實體的。
這顯然會帶來不少的問題,下面來具體的探討一下;
簡單地將屬性複製到目標對象中有一些問題。固然問題能夠經過足夠完備的 mixin
函數來解決:
1.Prototypes are modified in place.
當對原型對象使用 mixin
庫時,原型會被直接改變。若是在任何其餘不須要使用mixin
來的屬性的地方使用這個原型,那麼就會出現問題。
2.super
doesn't work.
既然JavaScript
最終支持super
, mixin
也應該支持。不幸的實際上咱們上面所實現的 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
已直接擴展到目標對象中。
瞭解了 mixin
這種模式的短處以後讓咱們來看看改進版。讓咱們快速列出咱們想要啓用的功能,以便咱們能夠根據它們來設計咱們的實現:
Mixin
應該是被添加到子類和超類之間的中間類,因此在 Javascript
中 Mixin
應該被添加到原型鏈中。Mixins application
不須要修改現有的對象。Mixins application
時不會修改子類。super.foo
屬性訪問適用於 mixin
和子類。Super()
調用超類(A
not A with M
)構造函數。Mixins
能夠繼承於其餘 Mixins
。instanceof
有效果。上面我將 mixin
稱爲**「由超類進行參數化的子類工廠」**,在實際的實現中其實就是這樣。
咱們依賴於JavaScript
類的兩個特性來實現這個子類工廠:
類能夠用做表達式,也能夠用做語句。做爲表達式,它在每次求值時返回一個新類。
let A = class {};
let a = new A(); // A {}
複製代碼
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 Definition
和 SuperClass
的交集的時候:
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
則做爲 MyClass
和 superClass
的中間層,擁有類實體。其自己經過 __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 */
}
複製代碼
來看看這種方法實現的 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
複製代碼
構造函數是形成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');
複製代碼
上面的代碼都是 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
是一個返回實例類型爲 X
的 mixin
超類構造函數類型。有了這個類型再對 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
接口的任何類(即具備類型爲 number
的 x
和 y
屬性)。
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];
}
};
複製代碼