TypeScript相关知识点整理_类

152 阅读10分钟

类 class

作为类型存在,类似于接口
作为值存在,类似于构造函数
类按照继承特性来说存在父类(基类);子类(派生类)
基类通常为抽象类,抽象一系列通用方法属性,具体实现由子类确定

类的结构

  • 静态属性和原型方法
    • 静态属性是具备static修饰符的属性,只能由类本身调用
    • 原型方法是不具备static修饰符的所有类中定义的方法,可以被实例和类本身调用
  • 静态部分和实例部分
    • 静态部分:默认存在于class中的构造器
    • 实例部分:在方法中用 this 指代的属性,或者class内定义的不带有 static 修饰符的属性及原型方法
  • 作为值存在,静态部分(因为至少存在构造器)必须具备
  • 作为类型存在,只会将实例部分作为类型结构
class TestClass {
    x: string;   //实例对象属性,es2022新写法,所有实例对象属性可以放在非constructor中直接定义

    constructor(x:string) { //必然会有constructor构造器函数,没有显式定义会默认存在空constructor
        this.x = x
    }

    toString() { //和constructor一样定义在类原型对象上,所有实例对象共享的原型方法

    }
}
// 上述写法对应构造函数写法
function TestClass(x){
    this.x = x
}
TestClass.prototype = {
  constructor() {},
  toString() {}
};

let testclass1 = new TestClass('a');

console.log(testclass1.hasOwnProperty('x'));   //true ,属于实例属性
console.log(testclass1.hasOwnProperty('toString'));   //false   实例对象本身不具备该属性

类的封装,继承,多态以及实现

封装:利用类成员修饰符

不加修饰符默认为public
对于不想暴露给外部又希望实现的方法属性利用private修饰符进行封装
仅针对于TS编写,编译过程中进行校验规范的封装特性,实际编译成JS以后的成员没有此限制,均可访问

  • public:公共属性
    • 可被自身调用
    • 可被派生类调用
    • 可被实例调用
  • private: 私有属性
    • 仅可被自身调用
    • 类似于构造函数中的变量赋值语句
  • protected
    • 可被自身调用
    • 可被派生类调用
    class Father {
        name: string;
        constructor(name: string) {
            this.name = name;
        }
        protected say(word: string = 'nothing') {
            console.log(word);
            
        }
    }
    
    class Son extends Father {
        constructor(name: string) {
            super(name)
        }
        private talk(word: string = 'anything') {
            console.log(word);
            
        }
        public testTalk(outWord: string) {
            this.talk(outWord)
        }
        say(word: string = 'anything') {
            super.say(word)   //父类protected 子类可调用
        }
    }
    
    let father = new Father('rock');
    // father.say('hello');    //父类protected,实例不可调用
    
    let son = new Son('nicky');
    // son.talk()              //私有属性,实例不可调用
    son.testTalk('outsideThing')             //outsideThing
    

继承:

  • 继承具备从属关系,一个类只能继承一个基础父类
class Apartment extends Building {}
  • 子类继承自父类,具备父类的实例属性,原型方法
  • 继承只关注可访问性修饰符。当父类成员的可访问性为protectedpublic时,不论其是否为静态或只读属性都可被继承

实现:

  • 实现无从属关系,可以有多个实现
class Apartment implements Price, Area, ZoneSize {}
  • 类可以实现 抽象类的抽象属性方法
  • 类可以实现 接口

多态:不同子类和父类的同名属性可以有不同实现

     //父类;基类
    class Animal {
        name: string;
        constructor(theName: string) { this.name = theName; }
        move(distanceInMeters: number = 0) {
            console.log(`${this.name} moved ${distanceInMeters}m.`);
        }
    }
    
    //子类;派生类
    class Snake extends Animal {
        constructor(name: string) { super(name); };
        move(){
            console.log('just want to climb')
        }
    }

    class Horse extends Animal {
        constructor(name: string) {super(name)};
        move(dis = 10) {
            super.move(dis) //等价于Animal.move(dir)
        }
    }
    

注意:

  • 类的多态性使得子类可以有各自的实现,但子类继承自父类的方法在封装方式(修饰符的使用)上必须保持一致
  • 父类constructor用private封装以后,不可被实例,也无法被继承
  • 父类constructor用protected封装以后,不可被实例,只能被继承
  • 当类作为类型使用时,其实例属性只能被public封装,只要存在非公共的实例成员,此类不可作为类型使用,因为私有属性不可被外部访问,因此也无法正常赋值

类修饰符

修饰符仅用在成员属性前

静态修饰符: static

  • TS和ES6中都支持的修饰符,是JS运行时修饰符
  • 表示属性为 内置属性 仅可被类自身调用,不是实例属性,也不会作为类型结构部分
  • 对于使用 static 修饰的属性方法,类更像是一个命名空间

可访问性修饰符: public, private, protected

  • 此修饰符优先级最高,要优先保证属性的可访问性,才能设定是否只读
  • 继承publicprotected的子类都可实现多态,但protected确保子类 多态 的同时,不会暴露属性

读写性修饰符: readonly

此修饰符表示属性只可读,仅能在类声明过程中对属性赋值,实例对象不可修改该属性

修饰符特别用法:

  • 配合方法传参可以简化属性的定义方式
    class SimplifyPro {
        constructor(readonly name: string) {
        }
    }
    
    //等效于
    class SimplifyPro {
        readonly name: string;
        constructor(name: string) {
            this.name = name
        }
    }
    

类存取器

  • 适用于ES5之后的语法,ES3不支持
  • 存取器由getter取值/setter存值函数构成
  • 目的:利用存取器封装一系列监听拦截的方法对内置(private修饰),受保护(protected修饰)或静态属性(static修饰) 进行有限制的对外暴露
    /** 私有属性的存取器 */
    class Person {
        constructor(private _name: string) {}
        get yourName():string {
            return this._name   //私有属性不能被访问,但通过get取值函数可以获得
        }
        set yourName(name: string) {   //不能具有返回值的类型批注---因为修改值并不确定类型
            this._name = name
        }
    }

    let jake = new Person('jake');
    // jake._name    //报错,私有属性不能被实例对象访问
    console.log(jake.yourName);   //jake
    jake.yourName = 'Bob';
    console.log(jake.yourName);   //Bob
    
    /** 静态属性的存取器 */
    class StaticClass0 {
        static myName: string = 'myName';
        get staticName() {
            return StaticClass0.myName
        }
        set staticName(name) {
            StaticClass0.myName = 'outer-' + name
        }
    }

    // 1.实例调用
    let staticClass0 = new StaticClass0();
    console.log(staticClass0.staticName); //myName
    //2.type
    let staticType1: StaticClass0 = {
        // myName: '1',   //error:不能有静态属性,对象字面量赋值时类的静态部分不会作为类型约束
        get staticName() {
            return StaticClass0.myName
        },
        set staticName(name) {
            StaticClass0.myName = name
        }
    }

抽象类 & 抽象属性/方法

可用抽象类表述某个实例的形状, 再用子类实例化 通常抽象类用于定义类型,派生类用于作为构造函数实现具体功能

定义及使用

  • 抽象类&抽象方法/属性 作用:当强制每个派生类都具备某种功能方法,通用父类又无法提供各具特性的实现,就需要 抽象类 进行 功能的抽象(抽象方法),从而引导子类进行具体实现功能

  • 可以理解 抽象 就是 强制 派生类必须有具体实现 的指令

  • abstract关键字声明的类为抽象类

  • 抽象类中使用abstract关键字声明的 属性/方法 为 抽象属性/方法

  • 抽象类只是一些公共属性和方法的集合,本质无具体实现,因此无法创建实例,但仍可以作为类型结构使用

  • 只有抽象类中才可存在抽象方法

    abstract class Menu0 {
        //抽象方法
        abstract setName(name: string): void;
        
        //原型方法
        arrayGetName: () => string;
        getName():string {
            return 'menu0'
        }
    }
    
    class FruitMenu0 extends Menu0 {
        setName(name: string): void { //必须有setName的具体实现,而非抽象方法可以不用重新定义
            console.log('fruit0');
            
        }
    }
    
    let menu0: Menu0 = {  //ok
        setName() {},
        getName() {return 'menu--instance'},
        arrayGetName() {return 'instance'}
    };
    
  • 抽象类不可被实例,但仍然存在构造器,可以被子类继承并super调用

        abstract class Class1 { constructor(a:number) { console.log(a); } } 
        class Class2 extends Class1 { 
            b:number; 
            constructor(a: number, b:number) { 
                super(a); //继承父类后可以使用其构造方法 
                console.log(b); 
            } 
        } 
        let class1 = new Class2(1,2);
    

子类extendsimplement 抽象类 的区别

  • 首先明确 抽象类 中 不一定 只有抽象方法,也可以有 具体实现的方法属性
  • 因此 子类 实现和继承的区别 主要是 对于抽象和非抽象的方法属性的区别:
    使用 extends ,表示 子类 仍然是 继承自 父类,父类的非抽象方法不用具体实现
    使用 implement 就需要 子类 实现所有父类抽象或非抽象的方法

扩展说明

设计模式中的 单例模式 和 类的结合

单例模式存在价值

  • 某个类只有唯一实例
  • 全局可调用此 类的实例,但无法生成或任意修改此实例
  • 保证数据的唯一性,如保存全局使用的数据库信息

单例模式创建方式

  • 私有构造器——防止对外调用生成新的实例
  • 静态生成实例方法——拦截并选择性 调用私有构造器生成唯一实例 或 获取缓存的唯一实例
  • 静态实例缓存——用于缓存唯一实例,不可被外部访问
class Ones {
  private static instance: Ones;
  private constructor(n: string) {}
  static getInstance(name: string) {
    if (!this.instance) this.instance = new Ones();
    return this.instance;
  }
}

其他

  • 类中默认存在constructor作为类型结构 和 接口 的区别 还在于 能通过构造方式赋值实例属性,所以类型定义时存在如下情况:
    //就算不写constructor,类默认有一个不接收传参的constructor,因此如下写法error 
    class Mod3 {
        //error
        exmname: string; 
        specname: string; 
    } 
    //由上,因缺乏构造器接收传参赋值,结构定义也没有给实例属性初始值,而类具备构造函数使用的方式可以改写为如下格式: 
    class Mod3 { 
        exmname: string | undefined; 
        specname: string | undefined;
        //or
        exmname = 'initial';  //类型推断为string
        specname = 'initial';
    }
    
  • 类如果采用修饰符传参方式定义成员属性,在构造函数中成员属性赋值优先于方法调用
    class Person {
        logName() {
            console.log(this.name);
            
        }
        constructor(readonly name : string) {
    					//name的获取优先于调用logName
            this.logName()   
        }
    }
    
    let person = new Person('jake');  //jake
    
  • 在tsconfig.json中将target设置为es5以下的版本,如es3,存取器的写法会报错
  • 作为es5语法的存取器不仅仅类中可用,使用Object.defineProperty设置一般属性的setter和getter后,此属性即为可存取属性,这种属性在读写的时候会调用get/set方法,因此可以对写入进行一定的条件限制
  • 构造签名构造函数类型等区别说明:用构造签名的方式可组成构造函数类型,拥有构造函数类型的函数为构造函数,此函数可用于创建实例对象
    • 构造签名 → 构造函数类型 → 构造函数 → 实例对象
    • 其中的 构造函数类型 可以由接口或类型声明的方式定义
    • 构造函数 可以由class定义
    • 因为类的静态部分constructor不是类型声明,无法作为类型被调用或检查, 因此需要借助 由接口或类型声明定义的 构造函数类型 以及 实例部分类型 封装的工厂函数来校验 构造函数 的构造器是否符合要求
    //实例部分类型
    interface Point {
      x: number;
      y: number;
    }
    
    //构造函数类型
    interface PointConstructor {
      new (x: number, y: number): Point;
    }
    
    
    //类定义的一个构造函数结构(包含实现)
    class Point2D implements Point {
      readonly x: number;
      readonly y: number;
    
      constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
      }
    }
    
    //一个具体实现的构造函数方法  ----工厂函数
    function newPoint(
      pointConstructor: PointConstructor,
      x: number,
      y: number
    ): Point {
      return new pointConstructor(x, y);
    }
    
    
    //使用了复合构造函数类型的class定义的构造函数结构创建的一个实例对象
    const point: Point = newPoint(Point2D, 2, 2);
    

遗留疑问

  • constructor和其他方法一样在原型上,可被实例调用?为什么在类作为类型的时候,constructor不会作为类型的一部分?