# 类的扩展

本节讲解基于接口,抽象类技术实现对象扩展的方法。

# 接口

之前章节已介绍可使用接口来描述对象类型,接口相比类,只创建了类型, 并不会有运行时开销。这里着重从 OOP 的角度进一步加深对接口的理解。

# implements

接口是类的骨架 确定了类的核心特性。接口抽象一套标准使不同类之间不用关心各类的具体实现,只需基于接口定义的规范来进行交互。一个现实案例就是 DOM 规范中的 IDL(接口设计语言) 描述 (opens new window) 通过一系列接口定义,确定 DOM 的内在行为。用户无需关心各浏览器厂商的实现差异,只需基于接口定义的规范即可操作 DOM。

在 TypeScript 利用 Interface 申明一系列规则,然后在类中使用 implements 去实现这些接口。

// 抽象文件接口,暴露读写方法
interface FileIO {
  // 返回 16 进制的字符串
  read(): string;
  // 写入 16 进制的字符串
  write(data: string): boolean;
}

// 文本读写 IO
class TextFileIO implements FileIO {
  read() {
    return 'text file';
  }
  write(data: string) {
    return true;
  }
}

// 图片文件读写 IO
class ImgFile implements FileIO {
  read() {
    return 'hex string data';
  }
  write(data: string) {
    return true;
  }
}

类可以 implements 多个接口的定义。

interface I1 {
  foo(): void;
}
interface I1 {
  bar(): void;
}

// C1 同时实现 I1 和 I2 定义的接口
class C1 implements I1, I1 {
  foo() {}
  bar() {}
}

TIP

注意接口用于说明类暴露的功能,所以接口定义的方法和属性只能为 public,不能设置为 private, protected。详情参见官方文档 interface class types (opens new window)

你可以使用 属性修饰 限制接口定义内容为只读或可选。

interface Hi {
  readonly name: string;
  sayHi(name?: string): void
}

class HiPerson implements Hi {
  private _name: string;

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

  get name() {
    return this._name;
  }
  sayHi() {
    console.log(`hi ${this.name}`)
  }
}
let hiPerson = new HiPerson('tom')
hiPerson.sayHi()

# 接口的扩展

和类一样接口也支持扩展

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

// 断言字面量对象符合 Square 接口特征,避免检查报错
let square = {} as Square;
square.color = 'blue';
square.sideLength = 10;

示例采用对象字面量形式创建接口而非类。体现了 JavaScript 对象创建的灵活性,你可以根据实际情况灵活组织代码。

# 多重扩展

你可以同时扩展多个接口的特性。

interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

// 同时扩展 Shape 和 PenStroke
interface Square extends Shape, PenStroke {
  sideLength: number;
}

let square1 = {} as Square;
square.color = 'blue';
square.sideLength = 10;
square.penWidth = 5.0;

# 接口扩展类

接口也可以扩展类。

class C11 {
  foo: string;
  constructor(foo: string) {
    this.foo = foo;
  }
}
// I11 继承了 C11 的所有方法和属性申明
interface I11 extends C11 {
  fooBar(): void;
}

// C12 必须实现 I11 扩充的方法和 C11 的方法和属性
class C12 implements I11 {
  foo: string;
  bar: string;
  constructor(foo: string, bar: string) {
    this.foo = foo;
    this.bar = bar;
  }
  fooBar() {}
}

示例中接口继承类会提取类中的方法和属性,类只需实现接口及提取类中对应的方法和属性即可。该机制基于接口协议实现了对依赖类的重写。

当依赖类中包含私有属性和方法时也可基于此机制来扩充特定子类。

class Control {
  private state: any;
}

// 定义 SelectableControl 在 Control 类上扩充 select 方法,并保留 state 私有属性
interface SelectableControl extends Control {
  select(): void;
}

// 由于需要包含 Control 私有属性 state 子类必须继承 Control 再补充实现接口才可以
class Button extends Control implements SelectableControl {
  select() {}
}

// 报错,SelectableControl 包含 Control 私有属性 state 。
// 无法在子类中覆盖 state,必须继承  Control  再实现接口补充方法才生效
class Image implements SelectableControl {
  private state: any;
  select() {}
}

# 抽象类

由于接口只是类型申明,类必须实现所有接口定义才可创建实例,抽象类结合了接口的约束及复用某些方法的实现的功能。采用 abstract 定义抽象类

abstract class AbstractAnimal {
  // 抽象方法,继承的类必须实现
  abstract makeSound(): void;
  
  // 抽象属性,继承的类必须实现
  abstract type: string;
  move(): void {
    console.log('roaming the earth...');
  }
}

class Tiger extends AbstractAnimal {
  type = 'Felidae';
  makeSound() {
    console.log('roar');
  }
}

let t = new Tiger();

t.move();
console.log(t);

// 报错, 抽象类不能实例化
console.log(new AbstractAnimal());

// 抽象类会产生运行时代码
console.log(Animal);

abstract 同样可以修饰,访问器属性。

abstract class Base {
  abstract name: string;
  abstract get value();
  abstract set value(v: number);
}

class Derived extends Base {
  name = 'derived';

  // 合法设置访问器属性为常规属性
  value = 1;
}

# 混入 (mixins)

混入是一种动态向类或对象实例注入方法的技术。有时我们会动态向实例混入多个类的方法和属性,为了避免混入报错采用 extends 类1,类2 {} 的方式申明扩展。

// 定义类 1
class Programer {
  code() {
    console.log('code');
  }
}
class Pianist {
  paly() {
    console.log('paly music');
  }
}
class Student {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

let tom = new Student('tom');

// 向 tom 实例动态注入 code 和 play 方法
mixinObj(tom, [Programer, Pianist]);

// 通过此申明实现 tom 的类型检查,注意申明顺序不会影响后续检查
// 注意不要忽略最后一个实例对象 {} 否则会报错!!!
// TODO: 为什么此处需要扩展 {}
interface Student extends Programer, Pianist {}

// 此处 tom
tom.code();
tom.paly();

function mixinObj(obj: any, baseCtors: any[]) {
  // 混入代码
}

你可以进一步阅读 Support for Mix-in classes (opens new window) 了解更多混入的信息。

# 总结

本章我们学习了如何基于接口和抽象类来进行继承。知识点如下:

  1. 接口是一种契约,定义类的使用规则。
    1. 类采用 implements 实现接口
    2. 类可同时继承多个接口的特性
    3. 接口可以互相扩展
    4. 接口基于类扩展,该操作会提取类成员的方法和属性申明
  2. 采用 abstract 定义抽象类,子类必须实现抽象类的抽象方法或属性
  3. 使用 extends 类1,类2 {} 申明对象混入