TypeScript 接口与实现:implements

在上一节学习 “抽象类” 时,我们知道了可以通过 abstract 关键字来为类制定一套强制性的底层 “契约”(比如规定支付类必须有 pay() 方法)。但抽象类有一个非常致命的限制:TypeScript 严格遵循单继承原则(一个子类只能 extends 一个父类)。

假如有这样一种业务场景:我们在开发一个智能家居系统。

  • 我们有一个基类叫 “门(Door)”,里面有 open() 和 close() 方法。
  • 我们需要开发一款 “防盗门(SecurityDoor)”。它显然是一个门,所以它应该 extends Door。
  • 但是,防盗门还需要具备 “报警(Alarm)” 的功能。而 “报警” 这个功能,不仅防盗门有,汽车(Car)也有,火灾探测器(SmokeDetector)也有。

如果我们把 “报警” 功能写进 Door 基类内部,那普通的木门也会报警,这显然不合理;如果我们专门搞一个 Alarm 父类,由于防盗门已经 extends Door 了,它就无法再继承 Alarm 类了。

面对这种 “跨越不同类系的公共能力”,TypeScript 为我们提供了面向对象体系中的终极解决方案:接口(interface)与 implements 关键字

TypeScript 中的 implements 是什么?

在 TypeScript 中,我们使用 implements 关键字来让一个类去实现某个接口。其中,implements 的字面意思是 “实现”。

  • extends(继承):代表的是一种 “是(is-a)” 的关系。比如防盗门 “是” 一种门。
  • implements(实现):代表的是一种 “具备(has-a)” 的关系。比如防盗门 “具备” 报警的能力。

其中,接口(interface)就像是一份纯粹的 “契约”,它里面绝对没有任何业务逻辑,只有方法和属性的签名。如果一个类签了这份协议(implements),就必须强制实现协议里规定的所有内容。

示例 1:使用 implements 实现接口

// 定义接口:报警功能
interface Alarm {
    // 接口里不能有具体的实现(不能写大括号)
    ring(): void;
}

// 定义父类
class Door {
    public open(): void {
        console.log("门打开了");
    }
    public close(): void {
        console.log("门关闭了");
    }
}

// 定义子类(防盗门):继承基类,并实现接口
class SecurityDoor extends Door implements Alarm {
    public ring(): void {
        console.log("[警报]: 滴滴滴!");
    }
}

const myDoor = new SecurityDoor();
myDoor.open();
myDoor.ring();

运行结果如下。

门打开了
[警报]: 滴滴滴!

分析:

在这个例子中,SecurityDoor 这个子类完美地融合了两个维度的特性:

  • 从父类 Door 继承了开关门的基础代码。
  • 接受了 Alarm 接口的严格约束,并自己动手实现了 ring() 方法。

这种设计方式,可以让代码更加解耦,也更加优雅。

TypeScript 使用 implements 实现多个接口

在 “类的继承” 这一节中我们说过,一个类只能 extends 一个父类。但是,一个类是可以 implements 无数个接口的。这就是在类型层面上解决 “多继承” 的最完美方案。

在 TypeScript 中,我们可以把各种独立的能力拆分成一个个小接口,然后使用 implements 像搭积木一样拼装到类的身上。

示例 2:一个类实现多个接口

// 能力 1:拍照
interface Camera {
    takePhoto(): void;
}

// 能力 2:播放音乐
interface MusicPlayer {
    playMusic(): void;
}

// 智能手机:它不是继承自某个万能父类,而是拼装了各种能力
class SmartPhone implements Camera, MusicPlayer {
    public name: string;

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

    // 必须实现 Camera 接口
    public takePhoto(): void {
        console.log(`${this.name} 拍了一张照片`);
    }

    // 必须实现 MusicPlayer 接口
    public playMusic(): void {
        console.log(`${this.name} 正在播放音乐`);
    }
}

const myPhone = new SmartPhone("Xiaomi 17 Pro");
myPhone.takePhoto();
myPhone.playMusic();

运行结果如下。

Xiaomi 17 Pro 拍了一张照片
Xiaomi 17 Pro 正在播放音乐

分析:

class SmartPhone implements Camera, MusicPlayer {}

我们在类名后面使用 implements Camera, MusicPlayer(用逗号隔开),此时就相当于给 SmartPhone 签了两份能力协议。任何少写、漏写、参数类型不匹配的行为,都会被 TypeScript 编译器当场拦截。

在上一节学习 “访问修饰符” 时,我们知道类有 public、private 和 protected 三种权限。那么,在实现接口时,我们可以把接口里的方法实现为私有的吗?

答案是:绝对不行!

接口(interface)本质上是一份 “对外的公开能力协议”。既然是对外公开的协议,那么它里面规定的所有属性和方法,在类中实现时,必须且只能是公有的(public)

interface Alarm {
    ring(): void;
}

class Car implements Alarm {
    // 错误操作:试图用 private 实现接口规定的方法
    private ring(): void {
        console.log("汽车报警响了!");
    }
    // 报错:类 “Car” 错误实现接口 “Alarm”
    // 属性 “ring” 在类型 “Car” 中是私有属性,但在类型 “Alarm” 中不是。
}

这是许多初学者在构建复杂系统时最容易踩的坑。小伙伴们要牢记一点:接口不关心你的私有财产,它只约束你对外展示的公共形象。

TypeScript 抽象类 vs 接口

学到这里,有小伙伴已经彻底晕了:“既然抽象类(abstract class)可以强制子类实现方法,接口(interface)也可以强制类实现方法,这两货之间到底有啥区别呢?我平时开发到底应该用哪个呢?”

这其实也是一道含金量极高的前端架构面试题。我们直接用一个对比表格来解决这个疑问:

抽象类 vs 接口
抽象类(abstract class) 接口(interface)
设计核心 它是 “半成品”,是对事物本质的抽象(是什么) 它是 “纯协议”,是对能力和行为的抽象(能干什么)
代码实现 可以包含实现代码(如普通方法和抽象方法) 绝对不能包含任何实现代码
属性定义 可以定义普通的实例属性。 只能定义属性的类型规范,不能赋初始值
继承数量 单继承(只能 extends 1 个) 多实现(可以 implements 多个)
应用场景 适用于同类事物的底层基础建设(如:各种支付方式的基类) 适用于给不同类系跨界赋予公共能力(如:让门和汽车都能报警)

实际上,接口不仅可以被类 implements,接口与接口之间也是可以 “相互继承” 的。而且,接口之间的继承是支持 “多继承” 的。比如我们可以写出这样的代码:

interface SmartPhone extends Telephone, Camera, GameConsole {}

也就是说,实际上 TypeScript 本身也给予了我们非常大的自由。

给站长反馈

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

邮箱:lvyenet@vip.qq.com

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