第六节:TypeScript类

Class 类

TypeScript 支持ES2015中引入的关键字class

与其他JavaScript语言功能一样, TypeScript也为class添加了类型注释和其他语法, 以允许你表达类和其他类型之间的关系


1. class 成员

这是一个基本的类,只是一个空类

class Point{
    
}

这个类还不是很有用,所以我们开始添加一些成员


1.1. 字段

1.1.1 字段(属性)的理解
字段声明在类上创建公共可写属性
class Point{
    // 这些字段在在类实例化后会成为实例对象的属性
    x: number  
    y: number
}

// 实例化
const pt = new Point()

pt.x = 0;
pt.y = 0;
console.log('pt',pt)
/*
    {x: 0,y: 0}
*/

类的成员x, y为公共的可写属性, 也就是类实例上的属性


与其他位置一样, 类型注释是可选的, 但是如果未指定,将默认为any类型

class Point{
    x;    // 类型: (property) Point.x: any
    y;    // 类型: (property) Point.y: any
}


字段也可以有初始器, 这些将在类被实例化时自动运行

class Point{
    x = 0;
    y = 0;
    // (property) Point.x: number
    // (property) Point.y: number
}

const pt = new Point()
console.log(`${pt.x}, ${pt.y}`); // 0 ,0

就像var,const, let定义变量一样, 类属性的初始器也将会用于推断其类型

如果赋值类型不匹配将报错

class Point{
    x = 0;
    y = 0;
}

const pt = new Point()
pt.x = '0'
// 不能将类型“string”分配给类型“number”

初始化赋值为number类型的值, 实例化后赋予string类型的值将警告


1.1.2 readonly 只读

字段可以使用readonly修饰符为前缀, 这样可以防止对构造函数之外的字段进行赋值

例如:

class Greeter {
    readonly name: string = 'world'

    constructor(otherName?:string){
        if(otherName !== undefined){
            this.name = otherName
        }
    }

    err(){
        this.name = 'not ok'
        // 报错:无法分配到 "name" ,因为它是只读属性。
    }
}

const g = new Greeter()
g.name = 'also not ok'
// 无法分配到 "name" ,因为它是只读属性。


只读属性值可以在constructor构造函数中进行修改赋值

例如:

const h = new Greeter('world')
console.log('h',h)
// {name: 'world'}

类中初始namehello在构造函数constructor被调用时修改为world


1.2 构造函数

类的构造函数与普通函数非常相似, 你可以添加带有类型注释, 默认值和重载的参数

例如:

class Person{
    name:string;
    age:number;

    // 构造函数
    // name 参数添加了类型注释
    // age 参数使用了默认值, 自动推断类型为number
    constructor(name:string, age = 18){
        // (parameter) age: number
        this.name = name  
        this.age = age
    }
}

在构造函数中, name属性, 添加了类型注释, age属性添加了默认值


构造函数也可以使用重载

class Person{
    name:string;
    age:number;
    className: number;  // 班级

    // 重载
    // 定义了两个重载, 一个接受两个参数, 第二个接受一个number类型参数
    constructor(name:string,age:number)
    constructor(className:number);
    // 重载的实现
    constructor(nameOrClassName:string | number, age = 18){
        if(typeof nameOrClassName == 'string'){
            this.name =  nameOrClassName
        }else{
            this.className = nameOrClassName
        }

        this.age = age
    }
}

类构造函数签名和函数签名之间只有一些区别:

  1. 构造函数不能有类型参数-他们属于外部类声明, 稍后了解
  2. 构造函数不能有返回类型注释- 类实例类型总是返回的


构造函数中super 关键字

注意,就像在JavaScript中一样, 如果有一个基类, 你需要使用super()来继承基类的属性或方法时, 在 任何this.成员之前调用你的构造函数体

例如

// 基类
class Base{
    age = 16
}


// 派生类
class Person extends Base{

    constructor(){
        // 错误的写法:访问派生类的构造函数中的 "this" 前,必须调用 "super"。
        this.age = 20
        super()
    }
}

在JavaScript中, 忘记调用super是一个很容易犯的错误, 但TypeScript会在必要时告诉你

例如:

// 派生类
class Person extends Base{

    constructor(){
        this.age = 20
        // 不使用super 报错:
        // 派生类的构造函数必须包含 "super" 调用。
    }
}


1.3 方法

类上的函数属性称为方法, 方法可以使用所有与函数和构造函数相同的类型注释

例如:

class Person{
    name:string
    age: number

    constructor(name:string, age:number){
        this.name = name
        this.age = age
    }

    // 方法(类实例化后,原型上的方法)
    sayHello(name:string):void{
        console.log(`hello ${name}, 我叫${this.name}`)
    }
}

const student = new Person('张三',18)
student.sayHello('李四')
// console: hello 李四, 我叫张三

除了标准的类型注释, TypeScript没有为方法添加任何新的东西


请注意,在方法体内, 仍然必须通过this.的方式调用主体中非限定名称的成员, 将始终引用封闭范围内的某些内容

例如:

let x:number = 10
class C{
    x:string = 'hello'

    m(){
        x = 'world'
        // 不能将类型“string”分配给类型“number”。
        // 不使用this.成员的方式, 直接使用x 查找的不是类成员,而是不同的变量x
    }
}

示例中调用的x并不是类主体范围内的x属性, 而是外部的变量x, 因此不能通过将string类型的值赋值给number类型的变量


1.4 getters / setters

类也可以有访问器

例如:

class C{
    _length = 0;

    // getter
    get length(){
        return this._length
    }

    // setter
    set length(value){
        this._length = value
    }
}

请注意,没有额外逻辑的由字段支持的 get/set 对在 JavaScript 中很少有用。如果您不需要在 get/set 操作期间添加其他逻辑,则可以公开公共字段。


Typescript对访问器有一些特殊的推理规则

  1. 如果get存在但不存在set, 则属性自动推断为readonly属性
  2. 如果不指定setter参数的类型, 则从getter的返回类型推断
  3. GetterSetter必须具有相同成员可见性


TypeScript4.3开始, 可以使用不同类型的访问器来获取和设置

例如:

class Thing {
  _size = 0;
 
  get size(): number {
    return this._size;
  }
 
  set size(value: string | number | boolean) {
    let num = Number(value);
 
    // Don't allow NaN, Infinity, etc
 
    if (!Number.isFinite(num)) {
      this._size = 0;
      return;
    }
 
    this._size = num;
  }
}

示例中, 在使用访问器设置参数可以为string, number, boolean的联合类型

但是因为属性是number类型, 所有需要在访问器中通过Number 转换类型


1.5 索引签名

类可以声明索引签名; 这与 其他对象类型的索引签名相同

处理构造函数, 类型中的成员都必须满足索引签名

例如:

class MyClass{
    // 索引签名: 属性的值为Boolean类型或一个方法接受一个string类型参数并返回boolean类型
    [s:string]: boolean | ((s:string) => boolean)

    // 属性
    isName = false

    // 方法
    check(s:string){
        return true
    }
}


如果不满足索引签名的匹配则报错

class MyClass{
    // 索引签名: 属性的值为Boolean类型或一个方法接受一个string类型参数并返回boolean类型
    [s:string]: boolean | ((s:string) => boolean)

    // 属性
    isName = 10
    // 报错: 类型“number”的属性“isName”不能赋给“string”索引类型“boolean | ((s: string) => boolean)”。ts(2411)

    // 方法
    check(s:string){
        return this[s]
    }
    // 报错:类型“(s: string) => boolean | ((s: string) => boolean)”的属性“check”不能赋给“string”索引类型“boolean | ((s: string) => boolean)”
}

索引签名类型还需要捕获方法的类型, 所以要有效的使用这些类型并不容易,

通常最好将索引数据存储在了另一个地方而不是类实例 本身


2. 类的实现

2.1implements实现

你可以使用implements关键字来检查一个类是否满足特定接口interface, 如果一个类未能正确实现这个接口, 则会发错错误

简单说:就是类中成员要实现接口定义成员

例如:

// 接口中声明了一个方法
interface Person{
    name: string  //接口中的属性
    sayHello():void  // 接口中的方法
}

// Student 类通过 implements 实现接口 Person 成员
class Student implements Person{
    // 实现接口中的属性
    name :string

    // 实现接口中的方法
    sayHello() {
        console.log('hello')
    }
}

// Doctor 未实现 接口中的sayHello 方法 ,因此 报错
class Doctor implements Person{
    say() {
        console.log('hello')
    }
}
/*
  类“Doctor”错误实现接口“Person”。
  类型 "Doctor" 中缺少属性 "sayHello",但类型 "Person" 中需要该属性。
*/

示例中Student实现了接口Person, 但是Doctor类没有实现, 所有报错, 告诉接口中需要sayHello属性, 但Doctor没有实现它


2.2 类实现多个接口

类也可以实现多个接口, 语法如下:

class C implements A, B {}
// C为类, A,B为接口

例如:

// 接口中声明了一个方法
interface Person{
    sayHello():void
}

// 接口
interface Base{
    name:string;
    age: number;
}

// 类实现两个接口
class Student implements Person,Base{
    // name,age 实现 Base接口
    name = 'hello';
    age = 18

    // sayHello 实现Person 接口
    sayHello() {
        console.log('hello')
    }
}


注意事项:

一个很重要的理解:implements关键字只是检查类是否可以被视为接口类型, 它根本不会改变类的类型或其他方法,一个常见的错误就是假设implement关键字会改变类的类型, 但是它不会

例如:

// 接口
interface Checkable{
  check(name:string):boolean
}

class NameCheck implements Checkable{
  check(s) {
      // (parameter) s: any
      // 注意此时参数那么为any类型, 并不会因为implements, 而导致参数类型发生改变(变为接口定义的参数类型) 
     return s.toLowercase() === 'ok'
  }
}

在这个例子中, 我们可以预计参数s的类型会受到接口中name:string参数的影响, 但implements不会更改检查类主体或推断其类型的方式

因此示例中,参数s还是按照正常的推断规则, 被推断为any类型


2.3 接口中存在可选属性

同样, 如果 接口中使用了可选属性,那么在实现接口时不会创建该属性

// 接口,age为可选属性
interface Person{
  name: string
  age?:number
}

// 1.类实现, age可选属性没有被实现
class Student implements Person{
  name: string
}

// 实例化类
const stu = new Student()
// 实例对象上不存在age属性, 因为类没有实现age属性
stu.age = 10

// Doctror实现了接口中的可选属性
class Doctor implements Person{
    name: string
    age:number
}


// 2.类实现了可选属性,实例对象上就可以操作可选属性
const doc = new Doctor()
doc.age = 20


3. extends 类继承

与其他具有面向对象特性的语言一样,JavaScript 中的类可以从基类继承。

类可能extends 继承基类, 派生类具有其基类的所有属性和方法, 并且还可以定义其他成员

例如:

// 基类
class Animal{
  move(){
    console.log('Moving along')
  }
}

// 派生类
class Dog extends Animal{
  woof(times:number){
    for(let i = 0; i< times; i++ ){
      console.log('woof!')
    }
  }
}

const dog = new Dog()
dog.move()
// Moving along
dog.woof(3)
// 3 woof!

示例中Dog类虽然没有定义move 方法, 但是其实例对象依然可以使用move方法

原因在于Dog类是extends继承基类Animal类的派生类, 其会自动继承基类的成员


3.1 覆盖方法

派生类也可以覆盖其基类字段或属性, 你可以使用super语法来访问基类方法,

TypeScript强制派生类始终是其基类的子类型

例如, 下面的这种覆盖方法是合法的方式

// 基类
class Base{
  greet(){
    console.log('hello world')
  }
}

// 派生类
class Derived extends Base {
  greet(name?:string){
    if(name === undefined){
      // 如果没有传递参数, 则调用父类的greet方法
      super.greet()
    }else{
      // 如果有参数, 则使用派生类自己的覆盖
      console.log(`Hello, ${name.toUpperCase()}`)
    }
  }
}

const d = new Derived()
d.greet()
// hello world
d.greet('reader')
// Hello, READER


派生类遵循其基类契约很重要, 请记住, 通过基类类型注释来引用派生类的实例是很常见的(而且总是合法的)

// 通过基类类型注释来应用派生类的实例
const b:Base = d;
b.greet()
// hello world
b.greet('jack')
// Hello, JACK

示例中,变量b的类型注释是基类Base, 但是赋值给变量b的确实派生类的实例对象


如果派生类不遵循基类的契约怎么办?

// 基类
class Base{
  greet(){
    console.log('hello world')
  }
}

// 派生类
class Derived extends Base {
  greet(name:string){
    console.log(`Hello, ${name.toUpperCase()}`) 
  }
}

// 错误:
/*
  类型“Derived”中的属性“greet”不可分配给基类型“Base”中的同一属性。
  不能将类型“(name: string) => void”分配给类型“() => void”
*/

示例中: 基类Base中greet 方法是没有参数的, 但是派生类Derived中greet方法的参数是必传参数,

此时TypeScript就会发出错误, 提示派生类中的属性greet不可分配给基类Base中的同一属性


如果此时还将派生类的实例赋值给使用基类类型注释的变量, 也将会发出错误

const b: Base = new Derived();
/*
  错误:
    不能将类型“Derived”分配给类型“Base”。
    属性“greet”的类型不兼容。
    不能将类型“(name: string) => void”分配给类型“() => void”
*/

示例中报错, 因为派生类和基类中的greet属性不兼容


3.2 仅类型字段声明

target >= ES2022或者useDefinedForClassFields这是为ture 时, 类字段在父类构造函数完成后初始化

覆盖父类设置的任何值, 当你只想为继承的字段重新声明更为精确的类型时, 这可能会成为问题

为了处理这种情况, 你可以使用declare向TypeScript表明这个字段声明不应该有运行时影响

例如:

// 接口
interface Animal{
  dataOfBirth:any
}


// 接口扩展
interface Dog extends Animal{
  breed: any
}

// 基类
class AnimalHouse{
  resident: Animal;

  constructor(animal:Animal){
    this.resident = animal
  }
}

// 派生类
class DogHouse extends AnimalHouse{
  //  不会对JavaScript 代码运行有任何影响
  // 只是让属性的类型更加精准
  declare resident:Dog;
  constructor(dog: Dog){
    super(dog)
  }
}


3.3 初始化顺序

在某些情况下, JavaScript类的初始化顺序可能会令人惊讶,

例如:

// 基类
class Base{
  name = 'base'
  constructor(){
    console.log('My name is '+ this.name)
  }
}

// 派生类
class Derived extends Base{
  name = 'derived'
}

const d  = new Derived()
// 打印: My name is base

示例中答应name这是base, 而不是derived


那这里发生了什么?

JavaScript定义的类初始化顺序是:

  1. 基类字段被初始化 name = 'base'
  2. 基类构造函数运行, Base中constructor 执行
  3. 派生类字段被初始化: name = 'derived'
  4. 派生构造函数运行

这意味着基类构造函数name在其自己的构造函数中看到了自己的值, 因为此时派生类字段初始化尚未运行


4. 类成员可见性

你可以使用TypeScript来控制某些方法或属性是否对类外部的代码可见


4.1 public 公共

类成员默认可见性是public, 可以在任何地方访问成员

例如

// 类
class Greeter{
  public greet(){
    console.log('hello')
  }
}

// 实例
const g = new Greeter()
g.greet()

因为public可见性修饰符 已经是默认的, 所以你不需要在 类的成员上编写此修饰符, 但有时可能出于样式/可读性的原因肯能会选择添加


4.2 protected 受保护的

protected成员仅对声明它们的类以及当前类的子类可见


// 类
class Greeter{
  // 公共的
  public greet(){
    console.log('hello, ' + this.getName())
  }

  // 受保护的
  protected getName(){
    return 'jack'
  }
}

// 派生类(子类)
class SpecialGreeter extends Greeter{
  public howdy(){
    console.log('howdy, ' + this.getName())
  }
}

// 基类的实例
const g = new Greeter()
g.greet()
g.getName()
// 错误: 属性“getName”受保护,只能在类“Greeter”及其子类中访问

// 派生类(子类)实例
const s = new SpecialGreeter()
s.howdy()
s.getName()
// 错误: 属性“getName”受保护,只能在类“Greeter”及其子类中访问

通过示例了解protected修饰的属性和方法,只能在类内部使用, 不能再类的实例上调用


protected 成员曝光

派生类需要遵循其基类的契约, 但可以选择公开具有更过功能的的基类子类型, 这包括是protected成员public

例如:

// 基类
class Base{
  protected num = 10
}

// 派生类
class Dervied extends Base{
  // 没有任何修饰符, 当前num就是public
  num = 50
}

const d = new Dervied()
console.log(d.num)  // 50

请注意, Dervied类中的num属性已经可以自由读写, 因此这要要注意, protected修饰的属性主要在派生类中,如果没有添加修饰, 当前同名属性将变为public, 如果这种暴露不是 故意的, 那么我们就需要小心重复修饰符

<>

4.3 private 私有的

private 就像protected, 但不允许子类访问该成员, 只能在当前类中访问

例如:

// 基类
class Base{
  private num = 10
}

// 派生类
class Dervied extends Base{
  showNum(){
    console.log(this.num)
    // 属性“num”为私有属性,只能在类“Base”中访问。
  }
}

// 基类实例化
const b = new Base()
console.log(b.num)
// 属性“num”为私有属性,只能在类“Base”中访问

// 派生类实例化
const d = new Dervied()
d.showNum()

示例中,发现private修饰的属性为私有属性, 只能在当前类中访问, 和protected很像

protected修饰符的属性为受保护的属性,只能在类中访问,可以是当前类也是派生类


因为private 成员对派生类不可见, 所以派生类不能增加其可见性(否则报错)

// 基类
class Base{
  private num = 10
  showNum(){
    console.log(this.num)
  }
}

// 派生类
class Dervied extends Base{
  num = 20
}

/*
  类“Dervied”错误扩展基类“Base”。
  属性“num”在类型“Base”中是私有属性,但在类型“Dervied”中不是
*/


跨实例访问private 成员

TypeScript允许跨实例访问private 成员

class A{
  private x = 10;
  
  sameAs(other:A){
    return other.x === this.x
  }
}

// 实例一
const a = new A()

// 实例二
const b = new A()

const bol = b.sameAs(a)
console.log('bol',bol)
// bol true


注意事项:

与TypeScript类型系统的其他方面一样,private,protected只在类型检查期间强制执行

这意味着像in或者简单的JavaScript运行时构造的属性查找任然可以访问private, protected成员

// 类
class MySafe{
  private num = 123
}

// 实例
const s = new MySafe()
console.log(s.num)
// TypeScript报错: 属性“num”为私有属性,只能在类“MySafe”中访问。
// 编译后, JavaScript运行, 依然可以答应出 123

private还允许在类型检查期间使用括号表示法进行访问。这使得private-declared 字段可能更容易访问单元测试等内容,缺点是这些字段是软私有的并且不严格执行隐私。

class MySafe{
  private num = 123
}

const s = new MySafe()
// 通过中括号访问不报错
console.log(s['num']) // 123


与TypeScript的private修饰符不同, JavaScript的私有字段(#) 在编译后仍然是私有的, 并且不提供前面提到的转移舱口(如括号符号访问), 这使得他们成为了硬私有的

class Person {
  #num = 10;
  name = '张三';
  constructor(){ }
}


// 实例化
const student = new Person()
console.log(student['#num'])  // undefined 获取不到值

如果您需要保护类中的值免受恶意行为者的侵害,您应该使用提供硬运行时隐私的机制,例如闭包、WeakMaps 或私有字段。请注意,这些在运行时添加的隐私检查可能会影响性能。


5. 静态成员

类可能有static成员, 这些成员不与类的特定实例相关联, 他们可以通过类构造函数对象本身访问:

例如:

// 类
class MyClass{
  static num = 10;
  static printNum(){
    console.log(MyClass.num)
  }
}

// 通过类名访问static静态成员
console.log(MyClass.num)
MyClass.printNum()


静态成员也可以使用相同的public, protected, 以及private修饰符

// 类
class MyClass{
  // 静态属性num 使用 了私有private 修饰符
  // 此时num 只能在MyClass类中访问
  private static num = 10;
  static printNum(){
    // ok
    console.log(MyClass.num)
  }
}



console.log(MyClass.num) 
// 错误: 属性“num”为私有属性,只能在类“MyClass”中访问。

MyClass.printNum()


静态成员也会被继承

// 基类
class Base{
  static getGreeting(){
    return 'hello world'
  }
}

// 派生类
class Derived extends Base{
  myGreeting = Derived.getGreeting()
}


5.1 特殊静态名称

Function从原型覆盖属性通常是不安全/不可能的, 因为类本省就是可以调用的函数. 所以不能将诸如类的名称, name, length, 和类函数属性call 定义为static成员

class Person{
  static name = 'hello'
  // 静态属性“name”与构造函数“Person”的内置属性函数“name”冲突
}


6.类中的 static 块(代码块)

静态块允许你编写具有自己范围的语句序列,这些语句可以访问包含类中的私有字段, 这意味着我们可以编写具有编写语句的所有功能的初始代码, 不会泄露变量, 并且可以完全访问我们类的内部结构

例如:

// 类型
class Foo{
  static #count = 0

  get count(){
    return Foo.#count;
  }

  static {
    // 静态快
    try {
      const num =  Math.floor(Math.random() * 10 )
      Foo.#count += num
  
    }catch(error){

    }
  }
}


7. 泛型类

类, 很像接口, 可以是泛型的,当使用实例化泛型类时, 其类型参数的推断方式与函数调用中方式相同

例如:

// 泛型类
class Box{
  contents: Type
  constructor(value: Type){
    this.contents = value
  }
}


// 实例化
const b = new Box("hello")
// const b: Box
console.log('b',b)

类可以像接口一样使用通用约束和默认值


7.1 静态成员中的类型参数

例:

// 泛型类
class Box{
  static defaultValue: Type
  // 错误: 静态成员不能引用类类型参数。
}

示例中的代码不合法, 原因在于类型总是会被完全擦除的

在运行时,只有一个Box.defaultValue属性槽, 这意味着设置Box.defaultValue(如果可能的话)也会被改变为Box.defaultValue, 这样就很不好,

泛型类的static成员 永远不能引用类的类型参数


8. 参数属性

TypeScript提供了特殊的语法,用于将构造函数参数转换为相同的名称和值的类属性, 这些称谓参数属性

是通过在构造函数参数前面加上修饰符public,protected, privatereadonly.来创建,

没有参数属性前,如果想让参数作为属性, 需要赋值处理

例如:

class Params {
  x: number;
  y: number;
  // 构造函数
  constructor(x:number,y:number){
    this.x = x;
    this.y = y;
  }
}

// 实例化
const a = new Params(10,20)
console.log(a) // {}


现在通过在参数前添加修饰符, 参数直接可以变成属性

例如:

// 类
class Params {

  constructor(
    public readonly x :number ,
    protected y:number ,
    private z:number
  ){
    // ...
  }
}

const a = new Params(1,2,3)
console.log(a) 
/*
  添加完修饰符,编译后运行, 浏览器打印的a对象上就有x,y,z三个参数属性
  {x: 1, y: 2, z: 3}
*/

// 1.x 属性为公共只读属性
console.log(a.x)
// (property) Params.x: number

a.x = 20
// 错误:无法分配到 "x" ,因为它是只读属性

// 2. y 属性为受保护属性, 实例上无法获取
console.log(a.y)
// 错误:属性“y”受保护,只能在类“Params”及其子类中访问 

// 3. z属性为私有的,只有在当前类中可以访问
console.log(a.z)
// 错误:属性“z”为私有属性,只能在类“Params”中访问。


10.类的表达式

类的表达式与类声明非常相似, 唯一真正的区别是类的表达式不需要定义名称, 我们可以通过他们最终绑定到的任何标识符来引用它们

例如:

// 类的表达式
const SomeClass = class {
  content:Type;

  constructor(value:Type){
    this.content = value
  }
}

// 实例化
const m = new SomeClass("hello world")
// const m: SomeClass


11 abstract抽象类和成员

11.1 抽象类和抽象成员

TypeScript中的类, 方法, 和字段可能是抽象的

抽象方法或抽象字段是尚未提供实现的方法, 有点像重载,只定义解构类型,

这些抽象成员必须存在于抽象类中, 不能直接实例化

抽象类的作用是作为实现所有抽象成员的子类的基类, 但一个类没有任何抽象成员时, 就说他是具体的类

例子:

// 抽象类
abstract class Base{
  // 抽象方法
  abstract getName():string

  printName(){
    console.log('Hello '+ this.getName())
  }
}

// 实例化抽象类
const b = new Base()
// 报错: 无法创建抽象类的实例

我们无法实例化Base, 因为他是抽象类, 相反,我们需要创建一个派生类并实现抽象成员

// 抽象类
abstract class Base{
    name: string
    constructor(name:string){
        this.name = name
    }
    // 抽象方法
    abstract getName():string

    printName(){
        console.log('Hello '+ this.getName())
    }
}


// 派生类: 实现抽象成员
class Student extends Base{
    age: number
    constructor(name:string,age:number){
        super(name)
        this.age = age
    }

    // 抽象成员的实现
    getName(): string {
        return this.name
    }
}

// 实例化派生类(实现类)
const b = new Student('小明',18)
b.printName()



请注意, 如果我们忘记实现基类(抽象类)的抽象成员, 我们会得到一个错误

// 派生类: 实现抽象成员
class Student extends Base{
  // 错误: 非抽象类“Derived”不会实现继承自“Base”类的抽象成员“getName”。
}

抽象类做为其他派生的基类使用,他们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节
abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法


11.2. 抽象构造签名

有时你想接受一些类构造函数, 它产生一个派生自某个抽象类的类的实例

例如:

// 抽象类
abstract class Base{
  // 抽象方法
  abstract getName():string

  printName(){
    console.log('Hello '+ this.getName())
  }
}

function greet(ctor: typeof Base){
  const instance = new ctor()

  // 报错: 无法创建抽象类的实例。

  instance.printName()
}

Typescript 正确的告诉您, 您正在尝试实例化一个抽象类,


相反, 你想编写一个接受带有构造函数签名函数

function greet(ctor: new () => Base){
  const instance = new ctor()
  instance.printName()
}

greet(Derived)
greet(Base)
/*
  报错: 
  类型“typeof Base”的参数不能赋给类型“new () => Base”的参数。
  无法将抽象构造函数类型分配给非抽象构造函数类型。
*/

现在 TypeScript 正确地告诉您可以调用哪些类构造函数 -Derived可以,因为它是具体的,但Base不能。


12. 类之间的关系

在大多数情况下, TypeScript中的类在结构上进行比较, 与其他类型相同

例如, 如下两个类可以相互替代使用, 因为他们是相同的

// 类1
class Point1{
  x = 0;
  y = 0;
}

// 类2
class Point2{
  x = 0;
  y = 0;
}

// OK
const p:Point1  = new Point2()

因为两个类相同, 所有可以将Point2类的实例化对象赋值给使用Point1类作为类型注释的变量


同样, 即使没有显示继承, 类之间的子类型关系也是存在

// 隐式的子类
class Person {
  name:string;
  age:number
}

// 隐式的父类
class Employee{
  name: string;
  age: number;
  salary: number;
}

// ok
const p:Person = new Employee()

父类的实例化对象可以赋值给使用子类作为类型注释的变量,

但是需要注意, 反过来就不可以,因为子类的实例化可能不满足父类中的成员


空类没有成员, 在结构类型系统中, 没有成员的类型通常是其他任何东西的超类型.

所以如果你写了一个空类(尽量不要), 那么任何东西都可以代替它

// 空类
class Empty{}

function fn(x:Empty){
  console.log(x)
}

// 以前全部ok
fn(window)
fn({})
fn(fn)


13. 类的其他用法

类除了可是实现接口外, 接口也可以反过来扩展类,

简单说就是可以把类当作接口使用

例如:

// 把类当成接口使用(相当于一个接口)
class Person{
  name:string
  age: number
}

interface Student extends Person{
  sex: string
}

let xiaoming:Student = {name:"小明", age: 18, sex: "男"}
console.log('xiaoming', xiaoming)

你可能感兴趣的:(第六节:TypeScript类)