抽象类是用 abstract 标记的基类。它不能用 new 创建,并且可以声明没有方法体的 abstract 方法,每个子类都必须实现这些方法。
Shape 把 describe() 共享给所有子类,同时强制每个子类提供 area()。类型 Shape[] 可以容纳各种子类的任意组合。
抽象类不能被实例化
对抽象类调用 new 会产生编译错误,因为这样得到的对象会带有没有实现的方法:
index.ts(6,11): error TS2511: Cannot create an instance of an abstract class.
这是一条编译时规则。abstract 会从输出中擦除,所以生成的 Shape 就是一个普通的 JavaScript 类,没有类型检查的 JavaScript 代码仍然可以调用 new Shape()。当一个库需要运行时保护时,可以在构造函数里检查 new.target:
抽象方法和抽象属性
子类必须实现每一个抽象成员,否则它自己也必须是抽象类。漏掉一个会报错 TS2515,内容类似 Non-abstract class 'Square' does not implement inherited abstract member area from class 'Shape';如果漏掉好几个,则会报 TS2654 并把它们全部列出来。
字段、getter 和方法都可以是抽象的,也都可以是 protected:
抽象方法没有方法体,给它写方法体会报错 TS1245(Method 'x' cannot have an implementation because it is marked abstract)。抽象类也不一定要有抽象成员:单独一个 abstract 只是用来阻止直接实例化。
模板方法模式
写抽象类最常见的理由是:基类固定步骤的顺序,每个子类填充其中会变化的步骤。
抽象类也可以是泛型的,就像这里一样,这样每个子类都能选择自己的记录类型。
抽象构造函数类型
对抽象类来说,typeof Shape 是一个不允许调用的构造函数,所以类型为 typeof Shape 的参数不能执行 new c()。当一个函数需要接收抽象类(比如注册它们或检查 instanceof)时,就用抽象构造签名来标注类型:
abstract new 同时接受抽象类和具体类;普通的 new 只接受具体类。
抽象类与接口的区别
| 抽象类 | 接口 | |
|---|---|---|
| 包含代码 | 是:方法、字段初始化、构造函数 | 否,只有类型 |
| 运行时存在 | 是 | 否,会被擦除 |
instanceof | 可用 | 不可用 |
| 一个类可以使用 | 一个(extends) | 多个(implements) |
| 访问修饰符 | private、protected、public | 只有公共成员 |
| 用作普通对象的类型 | 仅当没有 private/protected 成员时可以,而且此时 instanceof 会失败 | 可以,任何符合结构的值都行 |
经验法则:只需要描述结构时用接口(这是最常见的情况,而且对对象字面量也适用);子类之间共享真正的实现、并且你希望编译器强制它们补齐空缺时,用抽象类。两者也可以很好地结合:用接口定义公共契约,再用一个抽象类作为实现该接口的便捷基类。另一个常见的对比见 interface 与 type 的区别。
常见错误
- 该用接口的地方用了抽象类。 如果基类里没有任何代码,它就是一个多了运行时负担、还受单继承限制的接口。
- 尝试使用
static abstract。 这是不允许的,静态成员不能是抽象的。 - 以为对抽象类调用
new会在运行时报错。 除非你自己写了new.target检查,否则不会报错。 - 在基类构造函数里调用抽象方法。 它会在子类的字段初始化之前运行,所以实现代码看到的这些字段都是
undefined。
常见问题
TypeScript 中的抽象类是什么?
抽象类是用 abstract 标记的类,它不能用 new 创建,并且可以声明没有实现的 abstract 成员。子类必须实现每一个抽象成员。它让你既能共享代码,又能规定子类必须具备的结构。
TypeScript 中抽象类和接口有什么区别?
抽象类可以包含真正的代码(已实现的方法、带初始值的字段、构造函数),并且在运行时存在,所以可以对它使用 instanceof;一个类只能继承一个抽象类。接口没有代码,编译时会被擦除,一个类可以实现多个接口。要共享行为就用抽象类,只描述结构就用接口。
TypeScript 中的抽象类可以有构造函数吗?
可以。子类调用 super(...) 时它就会运行。但你仍然不能对抽象类本身调用 new(TS2511: Cannot create an instance of an abstract class)。
TypeScript 中的静态方法可以是抽象的吗?
不可以。abstract 只能用于实例成员;static abstract 会报错 TS1243('static' modifier cannot be used with 'abstract' modifier)。如果要强制要求某种静态结构,可以用一个包含该静态成员的接口来描述构造函数类型,并用它检查这个类。
抽象类在运行时会被强制执行吗?
不会。abstract 会被擦除,编译出来的类就是普通的 JavaScript,纯 JavaScript 代码仍然可以对它调用 new。如果这一点很重要,就在构造函数里检查 new.target 并抛出错误。