美文网首页js css html
第六节:TypeScript类

第六节:TypeScript类

作者: 时光如剑 | 来源:发表于2022-04-08 07:30 被阅读0次

    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<Type>{
      contents: Type
      constructor(value: Type){
        this.contents = value
      }
    }
    
    
    // 实例化
    const b = new Box("hello")
    // const b: Box<string>
    console.log('b',b)
    

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

    7.1 静态成员中的类型参数

    例:

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

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

    在运行时,只有一个Box.defaultValue属性槽, 这意味着设置Box<string>.defaultValue(如果可能的话)也会被改变为Box<number>.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<Type> {
      content:Type;
    
      constructor(value:Type){
        this.content = value
      }
    }
    
    // 实例化
    const m = new SomeClass("hello world")
    // const m: SomeClass<string>
    

    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类

        本文链接:https://www.haomeiwen.com/subject/cuwssrtx.html