類的裝飾器:ES6 中優雅的 mixin 式繼承

zaxx7760 8年前發布 | 22K 次閱讀 ECMAScript

前幾天翻譯了一篇文章 六個漂亮的 ES6 技巧 ,作者介紹了通過 ES6 的新特性實現的 6 種編程技巧。其中最后一種技巧是:“ Simple mixins via subclass factories ”,我翻譯成“通過子類工廠實現簡單的合成器”。限于我自身英文水平,也許把 mixin 翻譯成“合成器”并不是一個非常嚴謹的譯法,加上作者對這個技巧介紹的篇幅有限,所舉的例子比較簡單,因此有些同學表示看到這里覺得不太懂。然而,這個技巧又是體現了很重要的設計模式——mixin和裝飾器模式,因此我覺得有必要將它單獨拎出來詳細寫一篇文章。

ES5 的 mixin 模式

熟悉 JavaScript 的同學應該對 mixin 模式并不陌生。我們說 JavaScript / ES5 的繼承模型是基于單一原型鏈的繼承模型,通常情況下,在 JavaScript 實踐中完全用原型鏈來實現繼承式的代碼復用,是遠遠不能滿足需求的。因此實戰中,我們的代碼抽象基本上都是采用混合的模式,既有原型繼承,也有 mixin 組合。

那么什么是 mixin 呢? 最基本的 mixin 其實就是簡單地 將一個對象的屬性復制給另一個對象

function mixin(dest, src) {
    for (var key in src) {
        dest[key] = src[key]
    }
}

var person = {name: "akira", age: 25};
var student = {grade: 1};
mixin(student, person);

上面的代碼做的事情很簡單,就是枚舉出一個對象的所有屬性,然后將這些屬性添加到另一個對象上去。

有時候,我們需要將多個對象的屬性 mixin 到一個對象上去:

var dest = {...};
[src1, src2, src3].forEach(function(src){
    mixin(dest, src);
});

每次都用 forEach 操作顯然很繁瑣,因此通常情況下, mixin 考慮支持操作多個對象:

function mixin(...objs){
    return objs.reduce((dest, src) => {
        for (var key in src) {
            dest[key] = src[key]
        }
        return dest;    
    });
}

var dest = mixin({...}, src1, src2, src3);

在許多框架中,都有 mixin 的類似實現, jQuery 的 extend、YUI 有好幾個類似 mixin 的 API,lodash 中有 _.mixin 方法,npm 中的 mixin 模塊每個月有上千的下載。

jQuery 中的 extend

var object1 = {
  apple: 0,
  banana: { weight: 52, price: 100 },
  cherry: 97
};

var object2 = {
  banana: { price: 200 },
  durian: 100
};

// Merge object2 into object1
$.extend( object1, object2 );

// Assuming JSON.stringify - not available in IE<8
$( "#log" ).append( JSON.stringify( object1 ) );

ES5 中, mixin 為 object 提供功能“混合”能力,由于 JavaScript 的原型繼承能力,通過 mixin 一個或多個對象到構造器的 prototype,也能夠間接提供為“類”混合功能代碼的能力。

下面是 例子

function mixin(...objs){
    return objs.reduce((dest, src) => {
        for (var key in src) {
            dest[key] = src[key]
        }
        return dest;    
    });
}

function createWithPrototype(Cls){
    var P = function(){};
    P.prototype = Cls.prototype;
    return new P();
}

function Person(name, age, gender){
    this.name = name;
    this.age = age;
    this.gender = gender;
}

function Employee(name, age, gender, level, salary){
    Person.call(this, name, age, gender);
    this.level = level;
    this.salary = salary;
}

Employee.prototype = createWithPrototype(Person);

mixin(Employee.prototype, {
    getSalary: function(){
        return this.salary;
    }
});

function Serializable(Cls, serializer){
    mixin(Cls, serializer);
    this.toString = function(){
        return Cls.stringify(this);
    } 
}

mixin(Employee.prototype, new Serializable(Employee, {
        parse: function(str){
            var data = JSON.parse(str);
            return new Employee(
                data.name,
                data.age,
                data.gender,
                data.level,
                data.salary
            );
        },
        stringify: function(employee){
            return JSON.stringify({
                name: employee.name,
                age: employee.age,
                gender: employee.gender,
                level: employee.level,
                salary: employee.salary
            });
        }
    })
);

從一定程度上,mixin 彌補了 JavaScript 單一原型鏈的缺陷,可以實現類似于多重繼承的效果。在上面的例子里,我們讓 Employee “繼承” Person,同時也“繼承” Serializable。有趣的是我們通過 mixin Serializable 讓 Employee 擁有了 stringify 和 parse 兩個方法,同時改寫了 Employee 實例的 toString 方法。

我們可以如下使用上面定義的類:

var employee = new Employee("jane",25,"f",1,1000);
var employee2 = Employee.parse(employee+""); //通過序列化反序列化復制對象

console.log(employee2, 
    employee2 instanceof Employee,    //true 
    employee2 instanceof Person,    //true
    employee == employee2);        //false

ES6 中的 mixin 式繼承

在 ES6 中,我們可以采用全新的基于類繼承 “mixin” 模式設計更優雅的“語義化”接口,這是因為 ES6 中的 extends 可以繼承動態構造的類,這一點和其他的靜態聲明類的編程語言不同,在說明它的好處之前,我們先看一下 ES6 中如何更好地實現上面 ES5 代碼里的 Serializable:

用繼承實現 Serializable

class Serializable{
  constructor(){
    if(typeof this.constructor.stringify !== "function"){
      throw new ReferenceError("Please define stringify method to the Class!");
    }
    if(typeof this.constructor.parse !== "function"){
      throw new ReferenceError("Please define parse method to the Class!");
    }
  }
  toString(){
    return this.constructor.stringify(this);
  }
}

class Person extends Serializable{
  constructor(name, age, gender){
    super();
    Object.assign(this, {name, age, gender});
  }
}

class Employee extends Person{
  constructor(name, age, gender, level, salary){
    super(name, age, gender);
    this.level = level;
    this.salary = salary;
  }
  static stringify(employee){
    let {name, age, gender, level, salary} = employee;
    return JSON.stringify({name, age, gender, level, salary});
  }
  static parse(str){
    let data = JSON.parse(str);
    return new Employee(data);
  }
}

let employee = new Employee("jane",25,"f",1,1000);
let employee2 = Employee.parse(employee+""); //通過序列化反序列化復制對象

console.log(employee2, 
  employee2 instanceof Employee,  //true 
  employee2 instanceof Person,  //true
  employee == employee2);   //false

上面的代碼,我們用 ES6 的類繼承實現了 Serializable,與 ES5 的實現相比,它非常簡單,首先我們設計了一個 Serializable 類:

class Serializable{
  constructor(){
    if(typeof this.constructor.stringify !== "function"){
      throw new ReferenceError("Please define stringify method to the Class!");
    }
    if(typeof this.constructor.parse !== "function"){
      throw new ReferenceError("Please define parse method to the Class!");
    }
  }
  toString(){
    return this.constructor.stringify(this);
  }
}

它檢查當前實例的類上是否有定義 stringify 和 parse 靜態方法,如果有,使用靜態方法重寫 toString 方法,如果沒有,則在實例化對象的時候拋出一個異常。

這么設計挺好的,但是它也有不足之處,首先注意到我們將 stringify 和 parse 定義到 Employee 上,這沒有什么問題,但是如果我們實例化 Person,它將報錯:

let person = new Person("john", 22, "m");
//Uncaught ReferenceError: Please define stringify method to the Class!

這是因為我們沒有在 Person 上定義 parse 和 stringify 方法。因為 Serializable 是一個基類,在只支持單繼承的 ES6 中,如果我們不需要 Person 可序列化,而需要 Person 的子類 Employee 可序列化,靠這種繼承鏈是做不到的。

另外,如何用 Serializable 讓 JS 原生類的子類(比如 Set、Map)可序列化?

所以,我們需要考慮改變一下我們的設計模式:

用 mixin 實現 Serilizable

const Serializable = Sup => class extends Sup {
  constructor(...args){
    super(...args);
    if(typeof this.constructor.stringify !== "function"){
      throw new ReferenceError("Please define stringify method to the Class!");
    }
    if(typeof this.constructor.parse !== "function"){
      throw new ReferenceError("Please define parse method to the Class!");
    }
  }
  toString(){
    return this.constructor.stringify(this);
  }
}

class Person {
  constructor(name, age, gender){
    Object.assign(this, {name, age, gender});
  }
}

class Employee extends Serializable(Person){
  constructor(name, age, gender, level, salary){
    super(name, age, gender);
    this.level = level;
    this.salary = salary;
  }
  static stringify(employee){
    let {name, age, gender, level, salary} = employee;
    return JSON.stringify({name, age, gender, level, salary});
  }
  static parse(str){
    let data = JSON.parse(str);
    return new Employee(data);
  }
}

let employee = new Employee("jane",25,"f",1,1000);
let employee2 = Employee.parse(employee+""); //通過序列化反序列化復制對象

console.log(employee2, 
  employee2 instanceof Employee,  //true 
  employee2 instanceof Person,  //true
  employee == employee2);   //false

在上面的代碼里,我們改變了 Serializable,讓它成為一個動態返回類型的函數,然后我們通過 class Employ extends Serializable(Person) 來繼承可序列化,在這里我們沒有可序列化 Person 本身,而將 Serializable 實際上從 語義上 變成了一種修飾,即 Employee 是一種 可序列化的 Person 。于是,我們要 new Person 就不會報錯了:

let person = new Person("john", 22, "m"); 
//Person {name: "john", age: 22, gender: "m"}

這么做了之后,我們還可以實現對原生類的繼承,例如:

繼承原生的 Set 類

const Serializable = Sup => class extends Sup {
  constructor(...args){
    super(...args);
    if(typeof this.constructor.stringify !== "function"){
      throw new ReferenceError("Please define stringify method to the Class!");
    }
    if(typeof this.constructor.parse !== "function"){
      throw new ReferenceError("Please define parse method to the Class!");
    }
  }
  toString(){
    return this.constructor.stringify(this);
  }
}

class MySet extends Serializable(Set){
  static stringify(s){
    return JSON.stringify([...s]);
  }
  static parse(data){
    return new MySet(JSON.parse(data));
  }
}

let s1 = new MySet([1,2,3,4]);
let s2 = MySet.parse(s1 + "");
console.log(s2,         //Set{1,2,3,4}
            s1 == s2);  //false

通過 MySet 繼承 Serializable(Set),我們得到了一個可序列化的 Set 類!同樣我們還可以實現可序列化的 Map:

class MyMap extends Serializable(Map){
    ...
}

如果不用 mixin 模式而使用繼承,我們就得分別定義不同的類來對應 Set 和 Map 的繼承,而用了 mixin 模式,我們構造出了通用的 Serializable,它可以用來“修飾”任何對象。

我們還可以定義其他的“修飾符”,然后將它們組合使用,比如:

const Serializable = Sup => class extends Sup {
  constructor(...args){
    super(...args);
    if(typeof this.constructor.stringify !== "function"){
      throw new ReferenceError("Please define stringify method to the Class!");
    }
    if(typeof this.constructor.parse !== "function"){
      throw new ReferenceError("Please define parse method to the Class!");
    }
  }
  toString(){
    return this.constructor.stringify(this);
  }
}

const Immutable = Sup => class extends Sup {
  constructor(...args){
    super(...args);
    Object.freeze(this);
  }
}

class MyArray extends Immutable(Serializable(Array)){
  static stringify(arr){
    return JSON.stringify({Immutable:arr});
  }
  static parse(data){
    return new MyArray(...JSON.parse(data).Immutable);
  }
}

let arr1 = new MyArray(1,2,3,4);
let arr2 = MyArray.parse(arr1 + "");
console.log(arr1, arr2, 
    arr1+"",     //{"Immutable":[1,2,3,4]}
    arr1 == arr2);

arr1.push(5); //throw Error!

上面的例子里,我們定義了一個不可變數組,同時修改了它的序列化存儲方式,而這一切,通過聲明時 class MyArray extends Immutable(Serializable(Array)) 來實現。

總結

我們看到了 ES6 的 mixin 式繼承的優雅和靈活,相信大家對它強大的功能和非常漂亮的裝飾器語義有了比較深刻的印象了,在設計我們的程序模型的時候,我們可以開始使用它,因為我們有 Babel,以及 webpack 這樣的工具能將它編譯打包成 ES5,所以 ES6 早已不是什么虛無縹緲的東西。

記住 mixin 式繼承的基本形式:

const decorator = Sup => class extends Sup {
    ...
}

class MyClass extends decorator(SuperClass) {

}

最后,除了使用類的 mixin 式繼承之外,我們依然可以繼續使用普通對象的 mixin,而且我們有了 ES6 的新方法 Obejct.assign,它是原生的實現 mixin 對象的方法。

有任何問題,歡迎討論~

 

來自: https://www.h5jun.com/post/mixin-in-es6.html

 

 本文由用戶 zaxx7760 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!