談談 "JS 和 設計泛型"

曜靈SUN發表於2019-03-29

"泛型", 計算機程式設計中, 一個必不可少的概念。


簡單理解泛型

什麼是泛型

泛型是程式設計語言的一種特性。通過引數化型別來實現在同一份程式碼上操作多種資料型別。

對於強型別語言來書, 提到引數,最熟悉不過的就是定義 function A 時有形參,然後呼叫 A 時傳遞實參。 指定一個表示型別的變數,用它來代替某個實際的型別用於程式設計,而後通過實際呼叫時傳入型別 來對其進行替換,以達到一段使用泛型程式可以實際適應不同型別的目的。

注: 各種程式設計語言和其編譯器、執行環境對泛型的支援均不一樣

泛型解決的問題

  • 可重用性
  • 型別和演算法安全
  • 效率

這是非泛型類和非泛型方法無法具備的

常見的情形

你有一個函式,它帶有一個引數,引數型別是A,然而當引數型別改變成B的時候,你不得不復制這個函式

例如,下面的程式碼中第二個函式就是複製第一個函式——它僅僅是用String型別代替了Integer型別

func areIntEqual(x: Int, _ y: Int) -> Bool {
  return x == y
}

func areStringsEqual(x: String, _ y: String) -> Bool {
  return x == y
}

areStringsEqual("ray", "ray") // true
areIntEqual(1, 1) // true
複製程式碼

通過採用泛型,可以合併這兩個函式為一個並同時保持型別安全。下面是程式碼實現

// 用一個通用的資料型別T來作為一個佔位符,等待在例項化時用一個實際的型別來代替
func areTheyEqual(x: T, _ y: T) -> Bool {
  return x == y
}

areTheyEqual("ray", "ray")
areTheyEqual(1, 1)
複製程式碼

JavaScript和泛型的對應關係

泛型 和 模板方法(設計)模式

在一個系列的行為中,有一些是確定的,有一些是不明確的,我們把確定的行為定義在一個抽象類中, 不確定的行為定義為抽象方法,由具體的子類去實現,這種不影響整個流程,但可以應對各種情況的方法 就可以稱之為模板方法模式

demo - Coffee or Tea

幾個步驟:

  • 把水煮沸
  • 用沸水浸泡茶葉
  • 把茶水倒進杯子
  • 加檸檬
/* 抽象父類:飲料 */
var Beverage = function(){};
Beverage.prototype.boilWater = function() {
  console.log("把水煮沸");
};
Beverage.prototype.brew = function() {
  throw new Error("子類必須重寫brew方法");
};
Beverage.prototype.pourInCup = function() {
  throw new Error("子類必須重寫pourInCup方法");
};
Beverage.prototype.addCondiments = function() {
  throw new Error("子類必須重寫addCondiments方法");
};

/* 模板方法 */
Beverage.prototype.init = function() {
  this.boilWater();
  this.brew();
  this.pourInCup();
  this.addCondiments();
}
/* ------------分割線------------ */

/* 實現子類 Coffee*/
var Coffee = function(){};
Coffee.prototype = new Beverage();
// 重寫非公有方法
Coffee.prototype.brew = function() {
  console.log("用沸水沖泡咖啡");
};
Coffee.prototype.pourInCup = function() {
  console.log("把咖啡倒進杯子");
};
Coffee.prototype.addCondiments = function() {
  console.log("加牛奶");
};
var coffee = new Coffee();
coffee.init();

/* 實現子類 Tea*/
var Tea = function(){};
Tea.prototype = new Beverage();
// 重寫非公有方法
Tea.prototype.brew = function() {
  console.log("用沸水沖泡茶葉");
};
Tea.prototype.pourInCup = function() {
  console.log("把茶倒進杯子");
};
Tea.prototype.addCondiments = function() {
  console.log("加檸檬");
};
var tea = new Tea();
tea.init();
複製程式碼

這裡的Beverage.prototype.init就是所謂的模板方法

它作為一個演算法的模板指導子類以何種順序去執行哪些方法,在其內部,演算法內的每一 個步驟都清楚的展示在我們眼前

泛型 和 TypeScript

  • 泛型函式
  • 泛型類

TypeScript 為 JavaScriopt 帶來了強型別特性,但這就意味著限制了型別的自由度。同一段程式, 為了適應不同的型別,就可能需要寫不同的處理函式

而且這些處理函式中所有邏輯完全相同,唯一不同的就是型別——這嚴重違反抽象和複用程式碼的原則

泛型函式

js原始碼

var service = {
    getStringValue: function() {
        return "a string value";
    },
    getNumberValue: function() {
        return 20;
    }
};

function middleware(value) {
    console.log(value);
    return value;
}

var sValue = middleware(service.getStringValue());
var nValue = middleware(service.getNumberValue());
複製程式碼

ts改寫使用泛型

const service = {
    getStringValue(): string {
        return "a string value";
    },

    getNumberValue(): number {
        return 20;
    }
};
// 泛型方法改造
function middleware<T>(value: T): T {
    console.log(value);
    return value;
}

var sValue = middleware(service.getStringValue());
var nValue = middleware(service.getNumberValue());
複製程式碼

middleware 後面緊接的 表示宣告一個表示型別的變數,Value: T 表示宣告引數是 T 型別的, 後面的 : T 表示返回值也是 T 型別的

到這裡為止, TS改造之後的泛型方法和改造之前的js程式碼沒什麼區別。 現在的問題是 middleware 要怎麼樣定義才既可能返回 string,又可能返回 number,而且還能被型別檢查正確推匯出來? 如果不使用泛型方法要實現這個功能的程式碼實現:

第 1 個辦法,用 any:

function middleware(value: any): any {
    console.log(value);
    return value;
}
複製程式碼

這個辦法可以檢查通過。但它的問題在於 middleware 內部失去了型別檢查,在後在對 sValue 和 nValue 賦值的時候, 也只是當作型別沒有問題。簡單的說,是有“假裝”沒問題

第 2 個辦法,多個 middleware:

function middleware1(value: string): string { ... }
function middleware2(value: number): number { ... }
複製程式碼

或者用 TypeScript 的過載(overload)來實現

function middleware(value: string): string;
function middleware(value: number): number;
function middleware(value: any): any {
    // 實現一樣沒有嚴格的型別檢查
}
複製程式碼

這種方法最主要的一個問題是……如果我有 10 種型別的資料,就需要定義 10 個函式(或過載), 那 20 個,200 個呢……

泛型類

即在宣告類的時候宣告泛型,那麼在類的整個作用域範圍內都可以使用宣告的泛型型別, 多數 時候是應用於容器類

背景: 假設我們需要實現一個 FilteredList,我們可以向其中 add()(新增) 任意資料, 但是它在新增的時候會自動過濾掉不符合條件的一些,最終通過 get all() 輸出所有符合條件的資料(陣列)。 而過濾條件在構造物件的時候,以函式或 Lambda 表示式提供

// 宣告泛型類,型別變數為 T
class FilteredList<T> {
    // 宣告過濾器是以 T 為引數型別,返回 boolean 的函式表示式
    filter: (v: T) => boolean;
    data: T[];
    constructor(filter: (v: T) => boolean) {
        this.filter = filter;
    }

    add(value: T) {
        if (this.filter(value)) {
            this.data.push(value);
        }
    }

    get all(): T[] {
        return this.data;
    }
}

// 處理 string 型別的 FilteredList
const validStrings = new FilteredList<string>(s => !s);

// 處理 number 型別的 FilteredList
const positiveNumber  = new FilteredList<number>(n => n > 0);
複製程式碼

甚至還可以把 (v: T) => boolean 宣告為一個型別,以便複用:

type Predicate<T> = (v: T) => boolean;

class FilteredList<T> {
    filter: Predicate<T>;
    data: T[];
    constructor(filter: Predicate<T>) { ... }
    add(value: T) { ... }
    get all(): T[] { ... }
}
複製程式碼

相關文章