前言

借助一个模拟的场景来介绍接口的更多用法。
认识到接口虽然可以帮我们编写类提供约束,但是由于接口并不会生成到编译结果中,所以有时在使用接口时,ts 没法像 Java、C++ 写起来那样自然。

notes

接口用于约束类、对象、函数,是一个类型契约。

模拟一个场景

有一个马戏团,马戏团中有很多动物,包括:狮子、老虎、猴子、狗。

这些动物都具有的相同属性

  • 共同的特征:名字、年龄、种类名称
  • 共同的方法:打招呼

马戏团中有以下常见的技能

  • 火圈表演「IFireShow」:单火圈(singleFire)、双火圈(doubleFire)
  • 平衡表演「IWisdomShow」:独木桥(dumuqiao)、走钢丝(zougangsi)
  • 智慧表演「IBalanceShow」:算术题(suanshuti)、跳舞(dance)

马戏团中的动物都各自有各自的技能,狮子和老虎能进行火圈表演,猴子能进行平衡表演,狗能进行智慧表演。

未使用接口
不使用接口实现时,存在的一些问题:

  • 对成员函数(能力)没有强约束力
  • 容易将类型和能力耦合在一起

上面提及的这两个问题,本质原因在于系统中缺少对能力的定义。

接口的含义
面向对象领域中的接口语义:表达了某个类是否拥有某种能力。

某个类具有某种能力,其实就是实现(implements)了某种接口。

implements
上面提到的技能「火圈表演」、「平衡表演」、「智慧表演」,其实就是能力,它们应该定义为接口。

  1. class xxx implements xxx
  2. class xxx implements xxx, xxx
  3. class xxx extends xxx implements xxx
  4. class xxx extends xxx implements xxx, xxx

接口命名规范
通常在命名时,会以首字母 I 开头。

类型保护函数
通过调用该函数,会触发 TS 的类型保护,该函数必须返回 boolean。

  1. function func (xxx: 类型): xxx is 接口 {
  2. // ...
  3. return true; // true or false
  4. }

func(xxx) 结果

  • true 表示传入的 xxx 实现了接口
  • false 表示 xxx 没有实现接口

接口和类型别名的最大区别
接口可以被类实现
类型别名不能被类实现

接口继承类
接口可以继承类,表示该类的所有成员都在接口中。
其它面向对象语言不行,这是 ts 中特有的。

codes

  1. export abstract class Animal {
  2. abstract type: string;
  3. constructor(public name: string, public age: number) {}
  4. sayHello() {
  5. console.log(
  6. `各位观众,大家好,我是${this.type},我叫${this.name},今年${this.age}岁`
  7. );
  8. }
  9. }

将共同的东西提取到抽象类 Animal 中。

  1. export abstract class Animal {
  2. abstract type: string;
  3. constructor(public name: string, public age: number) {}
  4. sayHello() {
  5. console.log(
  6. `各位观众,大家好,我是${this.type},我叫${this.name},今年${this.age}岁`
  7. );
  8. }
  9. }
  10. export class Lion extends Animal {
  11. type: string = "狮子"
  12. }
  13. export class Tiger extends Animal {
  14. type: string = "老虎"
  15. }
  16. export class Monkey extends Animal {
  17. type: string = "猴子"
  18. }
  19. export class Dog extends Animal {
  20. type: string = "狗";
  21. }

这些动物实例继承自抽象类 Animal
必须要实现该抽象类中规定的抽象成员 type

  1. export abstract class Animal {
  2. abstract type: string;
  3. constructor(public name: string, public age: number) {}
  4. sayHello() {
  5. console.log(
  6. `各位观众,大家好,我是${this.type},我叫${this.name},今年${this.age}岁`
  7. );
  8. }
  9. }
  10. export class Lion extends Animal {
  11. type: string = "狮子";
  12. singleFire() {
  13. console.log(`${this.name}完成了单火圈表演`);
  14. }
  15. doubleFire() {
  16. console.log(`${this.name}完成了双火圈表演`);
  17. }
  18. }
  19. export class Tiger extends Animal {
  20. type: string = "老虎";
  21. singleFire() {
  22. console.log(`${this.name}完成了单火圈表演`);
  23. }
  24. doubleFire() {
  25. console.log(`${this.name}完成了双火圈表演`);
  26. }
  27. }
  28. export class Monkey extends Animal {
  29. type: string = "猴子";
  30. dumuqiao() {
  31. console.log(`${this.name}完成了独木桥表演`);
  32. }
  33. zougangsi() {
  34. console.log(`${this.name}完成了走钢丝表演`);
  35. }
  36. }
  37. export class Dog extends Animal {
  38. type: string = "狗";
  39. suanshuti() {
  40. console.log(`${this.name}完成了算术题表演`);
  41. }
  42. dance() {
  43. console.log(`${this.name}完成了跳舞表演`);
  44. }
  45. }

下面基于这个 animals.ts 来介绍我们没有使用接口来对“能力”进行约束所导致的一些问题。

未使用接口

  1. import { Animal, Dog, Lion, Monkey, Tiger } from "./animals";
  2. const animals: Animal[] = [
  3. new Lion("狮子", 1),
  4. new Tiger("老虎", 2),
  5. new Monkey("猴子", 3),
  6. new Dog("狗1", 4),
  7. new Dog("狗2", 5),
  8. ];
  9. // 要求1 所有动物打招呼
  10. animals.forEach(a => a.sayHello());
  11. // 要求2 所有具备「火圈表演」能力的动物进行「火拳表演」
  12. // ...

由于打招呼这个方法,是公共父类 Animal 身上的方法,所有的动物都会继承,都拥有该功能。
所以对于要求1,暂时还是没有啥问题的,写起来也很正常。是否使用接口对此影响不大。

image.png
要求2 所有具备「火圈表演」能力的动物进行「火拳表演」
狮子和老虎都能进行火圈表演能力。由于此时没有接口,现在存在以下问题:

  1. 没法约束 Lion、Tiger 都含有 singleFire、doubleFire 方法
  2. 不知道哪些动物具备「火圈表演」的能力

对于问题1,我们没法强行进行约束,无非就是通过文档或者注释的形式,来简单标注一下。
对于问题2,暂时只能凭借记忆里,在前面我们提到,狮子和老虎是具备「火圈表演」能力的,我们可以借此来判断。

  1. // 需求2 所有具备「火圈表演」能力的动物进行「火拳表演」
  2. animals.forEach((a) => {
  3. if (a instanceof Lion || a instanceof Tiger) {
  4. a.singleFire();
  5. a.doubleFire();
  6. }
  7. });

image.png
**a instanceof Lion || a instanceof Tiger**
这么写的问题:将「类型」和「能力」耦合在了一起

倘若有一天,马戏团中的狮子生病了,无法进行「火圈表演」,那么此时就需要将该能力给去掉。 按理来说,我们只需要修改 Lion 类,去掉它身上的 singleFire、doubleFire 函数即可「修改1」。 但是,按照上面这种写法,我们还需要修改“要求2”的写法:a instanceof Lion || a instanceof Tiger改为a instanceof Tiger「修改2」 在实际开发中,如果真遇到这种情况,那么「修改2」可远远不会只有一个地方需要改,很可能是多个文件都需要修改。

使用接口

  1. export interface IFireShow {
  2. singleFire(): void;
  3. doubleFire(): void;
  4. }
  5. export interface IWisdomShow {
  6. suanshuti(): void;
  7. dance(): void;
  8. }
  9. export interface IBlanceShow {
  10. dumuqiao(): void;
  11. zougangsi(): void;
  12. }
  1. import { IBlanceShow, IFireShow, IWisdomShow } from "./interfaces";
  2. export abstract class Animal {
  3. abstract type: string;
  4. constructor(public name: string, public age: number) {}
  5. sayHello() {
  6. console.log(
  7. `各位观众,大家好,我是${this.type},我叫${this.name},今年${this.age}岁`
  8. );
  9. }
  10. }
  11. export class Lion extends Animal implements IFireShow {
  12. type: string = "狮子";
  13. singleFire() {
  14. console.log(`${this.name}完成了单火圈表演`);
  15. }
  16. doubleFire() {
  17. console.log(`${this.name}完成了双火圈表演`);
  18. }
  19. }
  20. export class Tiger extends Animal implements IFireShow {
  21. type: string = "老虎";
  22. singleFire() {
  23. console.log(`${this.name}完成了单火圈表演`);
  24. }
  25. doubleFire() {
  26. console.log(`${this.name}完成了双火圈表演`);
  27. }
  28. }
  29. export class Monkey extends Animal implements IBlanceShow {
  30. type: string = "猴子";
  31. dumuqiao() {
  32. console.log(`${this.name}完成了独木桥表演`);
  33. }
  34. zougangsi() {
  35. console.log(`${this.name}完成了走钢丝表演`);
  36. }
  37. }
  38. export class Dog extends Animal implements IWisdomShow {
  39. type: string = "狗";
  40. suanshuti() {
  41. console.log(`${this.name}完成了算术题表演`);
  42. }
  43. dance() {
  44. console.log(`${this.name}完成了跳舞表演`);
  45. }
  46. }

要求2 所有具备「火圈表演」能力的动物进行「火拳表演」 狮子和老虎都能进行火圈表演能力。由于此时没有接口,现在存在以下问题:

  1. 没法约束 Lion、Tiger 都含有 singleFire、doubleFire 方法
  2. 不知道哪些动物具备「火圈表演」的能力

现在已经有接口了,再回看前面提到的这两个问题。
对于问题1,现在添加上接口实现后,就有了强约束。
对于问题2,需要借助类型保护函数来实现。

  1. // 需求2 所有具备「火圈表演」能力的动物进行「火拳表演」
  2. animals.forEach((a) => {
  3. if (a instanceof IFireShow) {
  4. a.singleFire();
  5. a.doubleFire();
  6. }
  7. });

**a instanceof IFireShow**
如果是在 Java、C++ 中,这种写法是完全 OK 的。可以直接通过判断 a 实例是否继承接口 IFireShow 来判断 a 是否具备 IFireShow 能力。
但是 ts 中没法这么写,因为在 ts 中,接口并不会生成到编译结果中。由于接口并不会生成到编译结果中,所以 TypeScript 没法像 Java、C++ 那样,让我们通过接口直接判断某个实例是否具备某个「能力」

  1. // 需求2 所有具备「火圈表演」能力的动物进行「火拳表演」
  2. const hasFireShow = (ani: object): ani is IFireShow => {
  3. if (
  4. (ani as unknown as IFireShow).singleFire &&
  5. (ani as unknown as IFireShow).doubleFire
  6. ) {
  7. return true;
  8. } else {
  9. return false;
  10. }
  11. };
  12. animals.forEach((a) => {
  13. if (hasFireShow(a)) {
  14. a.singleFire();
  15. a.doubleFire();
  16. }
  17. });

image.png
a 的类型被推断为 Animal & IFireShow 类型,只要是 Animal、IFireShow 中有的,实例 a 都能访问。

**ani is IFireShow**
如果函数的返回值是 true,那么表示类型保护函数认为 ani 是 IFireShow 类型
否则认为 ani 不是 IFireShow 类型

**ani as unknown as IFireShow** 断言 ani 是 IFireShow 类型。
unknown,该类型主要是为了保证类型安全出现的。在这里其实可以将 as unknown 给去掉,并不影响功能。

  1. const hasFireShow = (ani) => {
  2. if (ani.singleFire &&
  3. ani.doubleFire) {
  4. return true;
  5. } else {
  6. return false;
  7. }
  8. };
  9. animals.forEach((a) => {
  10. if (hasFireShow(a)) {
  11. a.singleFire();
  12. a.doubleFire();
  13. }
  14. });

image.png

需求3:去掉老虎的「火圈表演」技能,狗学会的「火圈表演」技能,让所有具备「火圈表演」技能的动物上台表演。

  1. import { IBlanceShow, IFireShow, IWisdomShow } from "./interfaces";
  2. export abstract class Animal {
  3. abstract type: string;
  4. constructor(public name: string, public age: number) {}
  5. sayHello() {
  6. console.log(
  7. `各位观众,大家好,我是${this.type},我叫${this.name},今年${this.age}岁`
  8. );
  9. }
  10. }
  11. export class Lion extends Animal implements IFireShow {
  12. type: string = "狮子";
  13. singleFire() {
  14. console.log(`${this.name}完成了单火圈表演`);
  15. }
  16. doubleFire() {
  17. console.log(`${this.name}完成了双火圈表演`);
  18. }
  19. }
  20. export class Tiger extends Animal {
  21. type: string = "老虎";
  22. singleFire() {
  23. console.log(`${this.name}完成了单火圈表演`);
  24. }
  25. doubleFire() {
  26. console.log(`${this.name}完成了双火圈表演`);
  27. }
  28. }
  29. export class Monkey extends Animal implements IBlanceShow {
  30. type: string = "猴子";
  31. dumuqiao() {
  32. console.log(`${this.name}完成了独木桥表演`);
  33. }
  34. zougangsi() {
  35. console.log(`${this.name}完成了走钢丝表演`);
  36. }
  37. }
  38. export class Dog extends Animal implements IWisdomShow, IFireShow {
  39. type: string = "狗";
  40. singleFire() {
  41. console.log(`${this.name}完成了单火圈表演`);
  42. }
  43. doubleFire() {
  44. console.log(`${this.name}完成了双火圈表演`);
  45. }
  46. suanshuti() {
  47. console.log(`${this.name}完成了算术题表演`);
  48. }
  49. dance() {
  50. console.log(`${this.name}完成了跳舞表演`);
  51. }
  52. }

image.png

该打印结果和预期的结果可能并不一致,上面提到了,需要将老虎的火圈表演能力给去掉,但是结果中依旧有老虎。
由此可见:implements IFireShow仅仅是对编写类时提供约束
class Tiger extends Animal implements IFireShow这种写法表示的含义,说得更加确切一些应该是:Tiger 类继承自 Animal 类,并且必须实现 IFireShow 接口。
class Tiger extends Animal这种写法也是可以实现 IFireShow 接口的,但是并没有强制约束。

为实现目前的需求3,我们还需要将 Tiger 中的 singleFire、doubleFire 给去掉才行。

接口继承类

  1. class A {
  2. a1: string = "";
  3. a2: string = "";
  4. }
  5. class B {
  6. b1: number = 0;
  7. b2: number = 0;
  8. }
  9. interface C extends A, B {}
  10. const c: C = {
  11. a1: "1",
  12. a2: "2",
  13. b1: 1,
  14. b2: 2,
  15. };