TypeScript 抽象类 (abstract)

我们都知道,通过类的继承,子类可以 “白嫖” 父类的属性和方法。但在真实的复杂业务中,有时候父类设计的概念是非常 “宽泛” 且 “模糊” 的。

举个简单例子,我们要开发一个收银系统。我们可能会设计一个父类叫 Payment(支付方式),然后派生出 WeChatPay(微信支付)和 AliPay(支付宝支付)两个子类。

在现实生活中,你可以使用微信支付,也可以使用支付宝支付,但你绝对不可能对收银员说:“我要使用 ‘支付方式’ 来付款”。因为 “支付方式” 只是一个抽象的概念,它并没有具体的落地形态。

因此在代码中,如果允许开发者使用 new Payment() 实例化一个模糊的父类对象,这是毫无意义且非常危险的。

为了解决这种 “只作为基石、不允许直接实例化”的场景,TypeScript 引入了架构师最爱的利器:抽象类(Abstract Class)

TypeScript 抽象类是什么?

在 TypeScript 中,我们只需要在 class 前面加上 abstract 关键字,那么这个类就变成了一个抽象类。

语法:

abstract class ClassName {
    // 1. 普通属性(子类可继承)
    public propertyName: type;

    // 2. 抽象方法(只有方法签名,没有具体实现 {},强制子类实现)
    abstract methodName(参数: 类型): 返回值类型;

    // 3. 普通方法(有具体逻辑,子类可直接复用)
    public normalMethod(): void {
        // 具体逻辑实现
    }
}

说明:

抽象类有且只有一条最根本的铁律:抽象类绝对不允许被实例化,也就是不能对其使用 new 关键字。抽象类的存在,天生就是为了被别人继承的。

示例 1:定义抽象类

// 定义一个抽象类(支付基类)
abstract class Payment {
    public name: string;

    constructor(name: string) {
        this.name = name;
    }

    public printName(): void {
        console.log("当前支付方式是:", this.name);
    }
}

// 子类继承抽象类
class WeChatPay extends Payment {
    // 子类继承了父类的非抽象方法
}

const wxPay = new WeChatPay("微信支付");
wxPay.printName();

运行结果如下。

当前支付方式是:微信支付

分析:

在这个例子中,我们在父类的前面加上 abstract 关键字,那么这个父类就变成了一个抽象类。如果我们尝试实例化这个抽象类,则 TypeScript 会报错拦截:

// 报错:无法创建抽象类的实例。
const p = new Payment("通用支付");

TypeScript 抽象方法

既然 TypeScript 抽象类不能实例化,那它除了提供公共方法供子类复用之外,还有什么更高级的用途呢?这就要引出抽象类中最核心的灵魂:抽象方法

在 TypeScript 抽象类中,我们可以使用 abstract 关键字来定义一个抽象方法。这个方法只有名字、参数和返回值类型,但没有任何具体的实现(即没有大括号 “{}”)。

抽象方法有点类似所谓的 “霸王条款”。如果一个子类继承了包含抽象方法的抽象类,那么子类必须(强制)去实现(重写)这个抽象方法!否则,TypeScript 编译器会立马报错。

示例 2:使用抽象方法

// 定义抽象类
abstract class BasePayment {
    // 普通方法:有具体的实现,子类可以直接白嫖
    public printReceipt(): void {
        console.log("[系统]: 正在打印小票...\n");
    }

    // 抽象方法:没有具体的实现,强制子类必须自己写具体的逻辑!
    abstract pay(amount: number): void;
}

// 子类 1:微信支付
class WXPay extends BasePayment {
    public pay(amount: number): void {
        console.log(`调用微信支付,扣款:${amount} 元`);
    }
}

// 子类 2:支付宝支付
class AliPay extends BasePayment {
    public pay(amount: number): void {
        console.log(`调用支付宝支付,扣款:${amount} 元`);
    }
}

const pay1 = new WXPay();
pay1.printReceipt();
pay1.pay(100);

const pay2 = new AliPay();
pay2.printReceipt();
pay2.pay(200);

运行结果如下。

[系统]: 正在打印小票...
调用微信支付,扣款:100 元

[系统]: 正在打印小票...
调用支付宝支付,扣款:200

分析:

就像是在团队开发中,架构师老大只需要写好一个 BasePayment 抽象类,并规定好 abstract pay(amount: number)。

然后在实际的业务开发中,业务小弟们不管是接入微信支付、支付宝支付还是银联支付,他们定义的子类都被迫必须提供一个 pay() 方法,并且参数必须是 number 类型。这样,我们就可以从根源上保证了整个项目组的代码规范高度统一。

TypeScript 使用抽象类实现 “多态”

在面向对象编程中,上面例子这种实现方式,也被称为 “多态”。所谓的 “多态”,指的是:同一操作作用于不同的对象,可以有不同的解释,从而产生不同的执行结果。

示例 3:使用抽象类与抽象属性实现多态

// 定义抽象类
abstract class BasePayment {
    // 1. 抽象属性:强制子类必须提供一个支付名称
    abstract name: string;

    // 2. 普通方法:有具体的实现,子类可以直接白嫖
    public printReceipt(): void {
        console.log(`[系统]: 正在使用 ${this.name} 打印小票...\n`);
    }

    // 3. 抽象方法:没有具体的实现,强制子类必须自己写具体的逻辑
    abstract pay(amount: number): void;
}

// 子类 1:微信支付
class WXPay extends BasePayment {    
    // 必须实现父类规定的抽象属性
    name: string = "微信支付";

    // 必须实现父类规定的抽象方法
    public pay(amount: number): void {
        console.log(`调用微信底层接口,扣款:${amount} 元`);
    }
}

// 子类 2:支付宝支付
class AliPay extends BasePayment {
    name: string = "支付宝";

    public pay(amount: number): void {
        console.log(`调用支付宝底层接口,扣款:${amount} 元`);
    }
}

// 核心:多态的威力
// 参数类型写成父类 BasePayment,它就能兼容接收所有继承了该父类的子类!
function processCheckout(paymentMethod: BasePayment, totalAmount: number): void {
    console.log("--- 准备结账 ---");
    // 不管传进来的是微信还是支付宝,统统调用统一的 pay 方法
    paymentMethod.pay(totalAmount);
    paymentMethod.printReceipt();
}

// 实例化具体的子类
const wx = new WXPay();
const ali = new AliPay();

// 结账时,传入不同的实例,产生不同的行为
processCheckout(wx, 100);
processCheckout(ali, 200);

运行结果如下。

--- 准备结账 ---
调用微信底层接口,扣款:100 元
[系统]: 正在使用 微信支付 打印小票...

--- 准备结账 ---
调用支付宝底层接口,扣款:200 元
[系统]: 正在使用 支付宝 打印小票...

分析:

在这个例子中,我们不仅使用了 abstract 约束了子类必须实现 pay() 方法,还通过 abstract name: string 强制要求子类必须提供名字。

最精妙的地方在于 processCheckout() 函数,它的参数声明为 BasePayment。这意味着,以后如果公司进行业务拓展,新增了 “银联支付”、“苹果支付”,我们只需要让新支付类继承 BasePayment,就可以直接把它们丢进 processCheckout 中运行,原来结账的底层逻辑连一行代码都不用改!

抽象类 vs 接口

学到这里,很多小伙伴会产生一个疑问:既然抽象类可以用来定义 “必须实现的契约”,那它和我们之前学的接口有什么区别呢?

记住最核心的一点:抽象类可以包含 “普通方法(带实现的逻辑)” 和 “属性”,它可以帮子类分担一部分基础建设;而接口(interface)是纯粹的规则,里面绝对没有任何逻辑实现,全都是抽象的规矩。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号