谈谈 "JS 和 设计泛型"

·  阅读 2660

"泛型", 计算机编程中, 一个必不可少的概念。


简单理解泛型

什么是泛型

泛型是程序设计语言的一种特性。通过参数化类型来实现在同一份代码上操作多种数据类型。

对于强类型语言来书, 提到参数,最熟悉不过的就是定义 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[] { ... }
}
复制代码

最后, 希望大家早日实现:成为前端高手的伟大梦想!!!

欢迎交流~

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改