ts中class创建泛型,this运行时注意事项及各种用法,类表达式,抽象类,类与类的关系

66 阅读6分钟

在class中创建泛型(类型变量)

关于何为泛型可以看我的另一篇文章泛型。此处重复一下创建类级别的泛型语法

class SomeClass<GenericType>{
  echo(target:GenericType){
   return target
  }
}

上面创建了一个类级别的泛型“genericType”

被static修饰的类变量不可使用类级别的泛型

class SomeClass<Type>{
 static someStaticProperty:Type //tatic members cannot reference class type parameters.
}

在类方法中的this

由于ts只提供类型检查,其并不会改变js在运行时的行为,因此,js中this在运行中存在的问题,使用ts也仍然存在,只不过ts在编译时期提供了一些类型检查,让我们尽可能让代码达到我们想要的结果。

函数中的this还是由调用它的对象决定,如

class SomeClass {
  p = 'this is SomeClass\'s property'
  getP(){
    return this.p
  }
}
const sc = new SomeClass()
const someObj = {
  p: "this is someObj's property",
  getP: sc.getP
}
console.log(someObj.getP()) // this is someObj's property

如上代码,我们使用someObj去调用SomeClass实例的getP方法,我们期望得到的结果是“this is SomeClass's property”但实际上却返回了“this is someObj's property”。

解决方式有两种

第一种,在class中使用箭头函数代替普通函数,如

class SomeClass {
  p = 'this is SomeClass\'s property'
  getP = () => {
    return this.p
  }
}
const sc = new SomeClass()
const someObj = {
  p: "this is someObj's property",
  getP: sc.getP
}
console.log(someObj.getP()) // this is SomeClass's property

ts在class中将普通函数换成箭头函数后,会产生一些比较重要的影响如下:

  1. 使用箭头函数将花费更大的内存,因为每个实例都会拥有函数的一个副本,如

image.png 可见SomeClass的实例会有一个getP函数的副本

  1. 在子类中无法通过super调用父类中的箭头函数(为在原型链上已经没有了该函数,而是变成了在每个实例中产生一个副本),如

1672750522680.png

会出现运行时错误

第二种,使用this传参,并且仍然使用普通函数

在ts中可以使用this作为类方法的参数,其将在运行时期被抹除,作用则为确保我们调用类方法时,调用者为类的实例,如

class SomeClass{ 
  name = "Some Class"
  getName(this: SomeClass,anotehrName?: string){
    return anotehrName ? anotehrName : this.name
  }
}
const sc = new SomeClass()
sc.getName('anotehrName')

const gn = sc.getName
gn() //The 'this' context of type 'void' is not assignable to method's 'this' of type 'SomeClass'
const obj = {
  getName: sc.getName
}
obj.getName() //The 'this' context of type '{ getName: (this: SomeClass, anotehrName?: string | undefined) => string; }' is not assignable to method's 'this' of type 'SomeClass'.

上面SomeClass的getName方法,第一个参数我们指定了this为SomeClass类型,如其调用者不是SomeClass的实例,让ts报错。

使用这种方式的影响:

  1. 我们可能无意间错误地使用this(比如无视编译错误,直接打包或运行等情况)
  2. 每个类只拥有其类方法的一个引用,而不是给每个类实例创建一个方法的副本
  3. 我们仍可以在子类中通过super调用父类方法,而不会产生运行时错误

使用this作为类方法的参数类型

this可以作为类方法的参数类型,其作用是限制传入的实例为本类的实例或其衍生类的实例,如

class Base {
  baseProperty = "b"
  someMethod(soemI: this){}
}

class BaseDerived extends Base{
  bdProperty = "bdp"
  someMethod(someI: this){
  }
  anotherMothod(){}
}

const b = new Base()
const bd = new BaseDerived()

b.someMethod(bd)
bd.someMethod(b) //Type 'Base' is missing the following properties from type 'BaseDerived': bdProperty, anotherMothod(2345)

this is "Type"

可以使用 this is + 类型 在类方法的返回类型位置,可以用来缩小实例的具体类型。想象这么一个场景,现在有三个类,FileSystemClass,DirectoryClass,FileClass。其中DirectoryClass,FileClass为FileSystemClass的子类。且子类上拥有各自的属性,我们对外提供了一个工具函数,接收用户传过来的子类实例,对Direcotry和FileClass会进行不同的逻辑处理,如

class FileSystemClass {
  isDirectory(){
    return this instanceof DirectoryClass
  }

  isFilePaths(){
    return this instanceof FileClass
  }
}

class DirectoryClass extends FileSystemClass{
  directoryProperty = "directoryProperty"
}

class FileClass extends FileSystemClass {
  fileProperty = 'fileProperty'
}

function utilFun(fsc:FileSystemClass){
  if(fsc.isDirectory()){
    //Directory的逻辑
    console.log(fsc.directoryProperty)// Property 'directoryProperty' does not exist on type 'FileSystemClass'.
  }else if(fsc.isFilePaths()){
    //File的逻辑
    console.log(fsc.fileProperty)// Property 'fileProperty' does not exist on type 'FileSystemClass'.
  }
}

可以看到utilFun中针对不同的文件类型需要用到具体类中的参数,但由于我们接收类型是父类FileSystemClass,其身上并不存在具体的属性,因此ts会报错。此时我们可以使用”this is type"来告诉ts,具体的判断中使用的是哪些子类,FileSystemClass代码修改如下:

image.png

此时,ts就能知道我们在fsc.isDirectory中使用的是子类DirectoryClass的内容,fsc.isFilePaths亦然。

class表达式

class同样可以书写成表达式的形式,如

const MyClass = class<Type>{
    classProperty = 'classProperty'
}

const mInstance = new MyClass()

抽象类和抽象类成员

ts中类允许拥有被abstract修饰的成员,即所谓的抽象成员,拥有抽象成员的类必须是抽象的,即class也得被abstract修饰,抽象类具有以下特点:

  1. 不能直接实例化抽象类,只能通过其子类实例化

  2. 其子类必须实现抽象类的所有抽象方法和拥有抽象类的所有抽象属性

image.png

声明抽象构造类型和非抽象构造类型

考虑以下场景,我们想要写一个工具函数,接收一个抽象类的子类,并返回其实例,你可能会如下书写:

abstract class AbstractClass {
  abstract name: string
}
const getInstance = (cons: typeof AbstractClass) => {
  return new cons() // Cannot create an instance of an abstract class
}

getInstance(AbstractClass)

从上代码可以发现,我们在方法上声明了一个抽象函数类型"typeof AbstractClass",虽然ts在getInstace内部报错了,调用者没发现其传了一个抽象类过来。 此时我们可以通过声明"非抽象构造函数类型",即 new () => 返回类型,方式让用户在调用时发现错误,如

abstract class AbstractClass {
  abstract name: string
}

const getInstance = (cons: new ()=> AbstractClass) => {
  return new cons()  
}

getInstance(AbstractClass) //Cannot assign an abstract constructor type to a non-abstract constructor type

ts中类与类的关系

大多数情况下,ts通过判断类的结构是否相同,来判断是否同一个类类型(typeof class),什么意思呢?如以下代码

class A {
    name = "A"
}
class B {
    name = "B"
}

function someFun(i: A){}
someFun(new B()) //这里并不会报错

上面代码ts会认为类A和类B的类型是相同。甚至只要某个类拥有另一个类的所有属性(类型也相同),则会被认为是另一个类的子类,即使你没有显示声明extends,如

class Base {
  name!: string
}

class AnotherClass{
  name!: string
  age!: number
}
const b:Base = new AnotherClass() // 这里没有显示extends但也不会报错

由于ts这种对比类型的方式,所以一个没有任何属性的类会被认为是所有类的父类,如

class Base {}

function someFun(i: Base){}

someFun({}) // 不会报错
someFun(window)// 不会报错