类与类成员的类型签名


一个函数的主要结构即是参数、逻辑和返回值,对于逻辑的类型标注其实就是对普通代码的标注,所以我们只介绍了对参数以及返回值地类型标注。而到了 Class 中其实也一样,它的主要结构只有构造函数属性方法访问符(Accessor,我们也只需要关注这三个部分即可。这里我要说明一点,有的同学可能认为装饰器也是 Class 的结构,但我个人认为它并不是 Class 携带的逻辑,不应该被归类在这里

属性的类型标注类似于变量,而构造函数、方法、存取器的类型编标注类似于函数:

  1. class Foo {
  2. prop: string;
  3. constructor(inputProp: string) {
  4. this.prop = inputProp;
  5. }
  6. print(addon: string): void {
  7. console.log(`${this.prop} and ${addon}`)
  8. }
  9. get propA(): string {
  10. return `${this.prop}+A`;
  11. }
  12. set propA(value: string) {
  13. this.propA = `${value}+A`
  14. }
  15. }

唯一需要注意的是,setter 方法不允许进行返回值的类型标注,你可以理解为 setter 的返回值并不会被消费,它是一个只关注过程的函数。类的方法同样可以进行函数那样的重载,且语法基本一致,这里我们不再赘述。
就像函数可以通过函数声明函数表达式创建一样,类也可以通过类声明类表达式的方式创建。很明显上面的写法即是类声明,而使用类表达式的语法则是这样的:

  1. const Foo = class {
  2. prop: string;
  3. constructor(inputProp: string) {
  4. this.prop = inputProp;
  5. }
  6. print(addon: string): void {
  7. console.log(`${this.prop} and ${addon}`)
  8. }
  9. // ...
  10. }

修饰符

在 TypeScript 中我们能够为 Class 成员添加这些修饰符:public / private / protected / readonly。除 readonly 以外,其他三位都属于访问性修饰符,而 readonly 属于操作性修饰符(就和 interface 中的 readonly 意义一致)。
这些修饰符应用的位置在成员命名前:

  1. class Foo {
  2. private prop: string;
  3. constructor(inputProp: string) {
  4. this.prop = inputProp;
  5. }
  6. protected print(addon: string): void {
  7. console.log(`${this.prop} and ${addon}`)
  8. }
  9. public get propA(): string {
  10. return `${this.prop}+A`;
  11. }
  12. public set propA(value: string) {
  13. this.propA = `${value}+A`
  14. }
  15. }
  • public:此类成员在类、类的实例、子类中都能被访问。
  • private:此类成员仅能在类的内部被访问。
  • protected:此类成员仅能在类与子类中被访问,你可以将类和类的实例当成两种概念,即一旦实例化完毕(出厂零件),那就和类(工厂)没关系了,即不允许再访问受保护的成员

静态成员

在 TypeScript 中,你可以使用 static 关键字来标识一个成员为静态成员:

  1. class Foo {
  2. static staticHandler() { }
  3. public instanceHandler() { }
  4. }


不同于实例成员,在类的内部静态成员无法通过 this 来访问,需要通过 Foo.staticHandler 这种形式进行访问。我们可以查看编译到 ES5 及以下 target 的 JavaScript 代码(ES6 以上就原生支持静态成员了),来进一步了解它们的区别:

  1. var Foo = /** @class */ (function () {
  2. function Foo() {
  3. }
  4. Foo.staticHandler = function () { };
  5. Foo.prototype.instanceHandler = function () { };
  6. return Foo;
  7. }());

从中我们可以看到,静态成员直接被挂载在函数体上,而实例成员挂载在原型上,这就是二者的最重要差异:静态成员不会被实例继承,它始终只属于当前定义的这个类(以及其子类)。而原型对象上的实例成员则会沿着原型链进行传递,也就是能够被继承。

而对于静态成员和实例成员的使用时机,其实并不需要非常刻意地划分。比如我会用类 + 静态成员来收敛变量与 utils 方法:

  1. class Utils {
  2. public static identifier = "linbudu";
  3. public static makeUHappy() {
  4. Utils.studyWithU();
  5. // ...
  6. }
  7. public static studyWithU() { }
  8. }
  9. Utils.makeUHappy();

继承、实现、抽象类

既然说到 Class,那就一定离不开继承。与 JavaScript 一样,TypeScript 中也使用 extends 关键字来实现继承:

  1. class Base { }
  2. class Derived extends Base { }

对于这里的两个类,比较严谨的称呼是 基类(Base派生类(Derived。当然,如果你觉得叫父类与子类更容易理解也没问题。关于基类与派生类,我们需要了解的主要是派生类对基类成员的访问与覆盖操作
基类中的哪些成员能够被派生类访问,完全是由其访问性修饰符决定的。我们在上面其实已经介绍过,派生类中可以访问到使用 public 或 protected 修饰符的基类成员。除了访问以外,基类中的方法也可以在派生类中被覆盖,但我们仍然可以通过 super 访问到基类中的方法:

  1. class Base {
  2. print() { }
  3. }
  4. class Derived extends Base {
  5. print() {
  6. super.print()
  7. // ...
  8. }
  9. }

在派生类中覆盖基类方法时,我们并不能确保派生类的这一方法能覆盖基类方法,万一基类中不存在这个方法呢?所以,TypeScript 4.3 新增了 override 关键字,来确保派生类尝试覆盖的方法一定在基类中存在定义:

  1. class Base {
  2. printWithLove() { }
  3. }
  4. class Derived extends Base {
  5. override print() {
  6. // ...
  7. }
  8. }

在这里 TS 将会给出错误,因为尝试覆盖的方法并未在基类中声明。通过这一关键字我们就能确保首先这个方法在基类中存在,同时标识这个方法在派生类中被覆盖了。
除了基类与派生类以外,还有一个比较重要的概念:抽象类。抽象类是对类结构与方法的抽象,简单来说,一个抽象类描述了一个类中应当有哪些成员(属性、方法等)一个抽象方法描述了这一方法在实际实现中的结构。我们知道类的方法和函数非常相似,包括结构,因此抽象方法其实描述的就是这个方法的入参类型返回值类型
抽象类使用 abstract 关键字声明:

  1. abstract class AbsFoo {
  2. abstract absProp: string;
  3. abstract get absGetter(): string;
  4. abstract absMethod(name: string): string
  5. }

注意,抽象类中的成员也需要使用 abstract 关键字才能被视为抽象类成员,如这里的抽象方法。我们可以实现(implements)一个抽象类:

  1. class Foo implements AbsFoo {
  2. absProp: string = "linbudu"
  3. get absGetter() {
  4. return "linbudu"
  5. }
  6. absMethod(name: string) {
  7. return name
  8. }
  9. }

此时,我们必须完全实现这个抽象类的每一个抽象成员。需要注意的是,在 TypeScript 中无法声明静态的抽象成员
对于抽象类,它的本质就是描述类的结构。看到结构,你是否又想到了 interface?是的。interface 不仅可以声明函数结构,也可以声明类的结构:

  1. interface FooStruct {
  2. absProp: string;
  3. get absGetter(): string;
  4. absMethod(input: string): string
  5. }
  6. class Foo implements FooStruct {
  7. absProp: string = "linbudu"
  8. get absGetter() {
  9. return "linbudu"
  10. }
  11. absMethod(name: string) {
  12. return name
  13. }
  14. }

在这里,我们让类去实现了一个接口。这里接口的作用和抽象类一样,都是描述这个类的结构。除此以外,我们还可以使用 Newable Interface 来描述一个类的结构(类似于描述函数结构的 Callable Interface):

  1. class Foo { }
  2. interface FooStruct {
  3. new(): Foo
  4. }
  5. declare const NewableFoo: FooStruct;
  6. const foo = new NewableFoo();