Class

静态成员

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

class Foo {
  static staticHandler() { }

  public instanceHandler() { }
}

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

var Foo = /** @class */ (function () {
  function Foo() {
  }
  Foo.staticHandler = function () { };
  Foo.prototype.instanceHandler = function () { };
  return Foo;
}());

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

OOP(继承、实现、抽象类)

继承与实现

与 JavaScript 一样,TypeScript 中也使用 extends 关键字来实现继承。

class Base { }
class Derived extends Base { }

基类中的哪些成员能够被派生类访问,完全是由其访问性修饰符决定的。派生类中可以访问到使用 public 或 protected 修饰符的基类成员。除了访问以外,基类中的方法也可以在派生类中被覆盖,但我们仍然可以通过 super 访问到基类中的方法。

class Base {
  print() { }
}

class Derived extends Base {
  print() {
    super.print()
    // ...
  }
}

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

class Base {
  printWithLove() { }
}

class Derived extends Base {
  override print() {
    // ...
  }
}

在这里 TS 将会给出错误,因为尝试覆盖的方法并未在基类中声明。通过这一关键字我们就能确保首先这个方法在基类中存在,同时标识这个方法在派生类中被覆盖了。

抽象类

抽象类是对类结构与方法的抽象,简单来说,一个抽象类描述了一个类中应当有哪些成员(属性、方法等),一个抽象方法描述了这一方法在实际实现中的结构

抽象类使用 abstract 关键字声明,另外在 TypeScript 中无法声明静态的抽象成员

abstract class AbsFoo {
  // 这里抽象类中的成员也需要使用 abstract 关键字才能被视为抽象类成员
  abstract absProp: string;
  abstract get absGetter(): string;
  abstract absMethod(name: string): string
}

对于抽象类,它的本质就是描述类的结构。同样 interface 不仅可以声明函数结构,也可以声明类的结构。

interface FooStruct {
  absProp: string;
  get absGetter(): string;
  absMethod(input: string): string
}

class Foo implements FooStruct {
  absProp: string = "aaa"

  get absGetter() {
    return "aaa"
  }

  absMethod(name: string) {
    return name
  }
}

私有构造函数

class Foo {
  private constructor() { }
}

当你想要实例化这个类时,就会出现报错:类的构造函数被标记为私有,且只允许在类内部访问。

有些场景下私有构造函数确实有奇妙的用法,比如把类作为 utils 方法时,此时 Utils 类内部全部都是静态成员,我们也并不希望真的有人去实例化这个类。此时就可以使用私有构造函数来阻止它被错误地实例化。

SOLID 原则

SOLID 原则是面向对象编程中的基本原则,它包括以下这些五项基本原则。

  • S,单一功能原则,一个类应该仅具有一种职责,这也意味着只存在一种原因使得需要修改类的代码。如对于一个数据实体的操作,其读操作和写操作也应当被视为两种不同的职责,并被分配到两个类中。更进一步,对实体的业务逻辑和对实体的入库逻辑也都应该被拆分开来。
  • O,开放封闭原则,一个类应该是可扩展但不可修改的。即假设我们的业务中支持通过微信、支付宝登录,原本在一个 login 方法中进行 if else 判断,假设后面又新增了抖音登录、美团登录,难道要再加 else if 分支(或 switch case)吗?
enum LoginType {
  WeChat,
  TaoBao,
  TikTok,
  // ...
}

class Login {
  public static handler(type: LoginType) {
    if (type === LoginType.WeChat) { }
    else if (type === LoginType.TikTok) { }
    else if (type === LoginType.TaoBao) { }
    else {
      throw new Error("Invalid Login Type!")
    }
  }
}

这当然不正确,基于开放封闭原则,我们应当将登录的基础逻辑抽离出来,不同的登录方式通过扩展这个基础类来实现自己的特殊逻辑

abstract class LoginHandler {
  abstract handler(): void
}

class WeChatLoginHandler implements LoginHandler {
  handler() { }
}

class TaoBaoLoginHandler implements LoginHandler {
  handler() { }
}

class TikTokLoginHandler implements LoginHandler {
  handler() { }
}

class Login {
  public static handlerMap: Record<LoginType, LoginHandler> = {
    [LoginType.TaoBao]: new TaoBaoLoginHandler(),
    [LoginType.TikTok]: new TikTokLoginHandler(),
    [LoginType.WeChat]: new WeChatLoginHandler(),

  }
  public static handler(type: LoginType) {
    Login.handlerMap[type].handler()
  }
}
  • L,里式替换原则,一个派生类可以在程序的任何一处对其基类进行替换。这也就意味着,子类完全继承了父类的一切,对父类进行了功能地扩展(而非收窄)
  • I,接口分离原则,类的实现方应当只需要实现自己需要的那部分接口。比如微信登录支持指纹识别,支付宝支持指纹识别和人脸识别,这个时候微信登录的实现类应该不需要实现人脸识别方法才对。这也就意味着我们提供的抽象类应当按照功能维度拆分成粒度更小的组成才对。
  • D,依赖倒置原则,这是实现开闭原则的基础,它的核心思想即是对功能的实现应该依赖于抽象层,即不同的逻辑通过实现不同的抽象类。还是登录的例子,我们的登录提供方法应该基于共同的登录抽象类实现(LoginHandler),最终调用方法也基于这个抽象类,而不是在一个高阶登录方法中去依赖多个低阶登录提供方