ts笔记

82 阅读7分钟

基本类型

  1. any unknown

    1. any 类型可以和任何类型交换,any类型在是对象时会自动推导

    2. unknown 直接接受其他类型 不能传递给其他类型

    3. unknown 类型为对象时,不能读属性和方法

    4. unknown 比any更安全

    5. // Any类型
      let notSure: any = 4;
      
  2. 隐式 any 类型

    1. ts 根据实际情况推导出的any类型
  3. Object Array (对象类型 数组类型)

    1. // 定义数组类型
      let list: number[] = [1, 2, 3];	
      let list: Array<number> = [1, 2, 3];
      // 定义元组类型
      let x: [string, number];
      x =  ['hello', 10];
      // 定义对象类型
      let a: object = {};
      function create(o: object): void {}
      // 数组解构
      function f([first, second]: [number, number]): void {}
      type C = { a: string; b: number };
      //  对象解构
      function c({ a, b }: C): void {}
      
  4. Number String Boolean

    1. // 定义布尔值类型
      let isDone: Boolean = false;
      
  5. number string boolean

    1. // 定义布尔值类型
      let isDone: boolean = false;
      // 定义数字类型
      let decLiteral: number = 6;
      // 定义字符串类型
      let name: string = "bob";
      
  6. 1 '2' false

    1. 字面量类型 会被进行类型推断

    2. let name = "bob";
      
  7. never

    1. never类型表示的是那些永不存在的值的类型

    2. // 定义never类型
      function error(message: string): never {
          throw new Error(message);
      }
      
  8. void

    1. void类型像是与any类型相反,它表示没有任何类型,一般用于没有返回值的函数

    2. function warnUser(): void {
          console.log("This is my warning message");
      }
      // 定义void类型 只能赋予undefined和null
      let unusable: void = undefined;
      
  9. null undefined

    1. // undefined和null
      let u: undefined = undefined;
      let n: null = null;
      
  10. Tuple

    1. 元组 用于存储数据类型不一致的数组类型

    2. let mytuple: [number, string, boolean] = [42, "Runoob", true];
      
  11. enum 枚举

    1. 既可以做为类型 也可以当做变量

    2. 枚举我们可以定义一些带名字的常量

    // 定义枚举
    enum Color {Red, Green, Blue}
    let c: Color = Color.Green;
    
  12. 函数类型

    1. // 写法1
      // 可以使用对象的形式处理,key是函数入参,value是函数返回值
      const a: { (): void } = () => {};
      
      const handle: { (): void } = function () {};
      
    2. // 写法二
      // 直接以函数格式约束
      const a: () => void = () => {};
      
    3. // 使用 构造类
      const handle: Function = function () {};
      
  13. 类型断言

    1. // 语法1 通过 <> 修改类型
      let someValue: any = "this is a string";
      let strLength: number = (<string>someValue).length;
      // 语法2 使用 as 关键字
      let someValue: any = "this is a string";
      let strLength: number = (someValue as string).length;
      

接口

  1. 接口用于约束类,对象,函数,是一个类型契约
  2. 在ts中接口是不参与编译的,在进行ts编译后会被删除
  • 基本使用

    • interface LabeledValue {
        label: string;
      }
      function printLabel(labeledObj: LabeledValue) {
        console.log(labeledObj.label);
      }
      let myObj = { size: 10, label: "Size 10 Object" };
      printLabel(myObj);
      
  • 只读属性

    • readonly 标记的属性. 初始化后不能再次修改

    • 
      interface Point {
        readonly x: number;
        readonly y: number;
      }
      // 创建之后 属性就不能修改了
      let pi: Point = { x: 10, y: 20 };
      // pi.x = 100
      
  • 定义属性为函数类型

    • interface SearchFunc {
        (source: string, subString: string): boolean;
      }
      let mySearch: SearchFunc;
      mySearch = function(src: string, sub: string): boolean {
          let result = src.search(sub);
          return result > -1; 
      }
      
  • 类型索引

    • 接口中的索引签名属性,必须是字符串或者数字类型

    • 表明该对象或者数组的 只能通过指定类型下标或者key 进行获取

    • interface StringArray {
          [index: number]: string;   // 指定元素key 类型为number 对应的value为string 
      }
      let myArray: StringArray;   
      myArray = ["Bob", "Fred"];
      let myStr: string = myArray[0];
      
  • 类型合并

    • 索引类型合并

    • class Animal {
          name: string;
      }
      class Dog extends Animal {
          breed: string;
      }
      interface NotOkay {
          [x : string]: Animal;
          [x : number]: Dog;
      }
      
  • 构造函数接口

    • // new 关键字需要执行构造函数 constructor
      interface ClockInterface {
        currentTime: Date;
        setTime(d: Date): Date;
      }
      // 描述创建Clock实例所需的构造函数
      interface ClockConstructor {
        new (hour: number, minute: number): ClockInterface;
      }
      
  • 接口继承类

    • // 接口继承类
      class Control {
          private state: any;
      }
      interface SelectableControl extends Control {
          select(): void;
      }
      class Button extends Control implements SelectableControl {
          select() { }
      }
      class TextBox extends Control {
          select() { }
      }
      // 必须通过继承Control类才能实现SelectableControl接口
      // 因为Control接口中声明了属性state,而SelectableControl类中没有。
      class ImageControl  implements SelectableControl {
          private state: any;
          select() { }    
      }
      
  • 混合类型

    • // 混合类型
      interface Counter {
      // 这是一个函数签名,意味着任何实现这个接口的对象都必须能够作为一个接受一个数字参数并返回一个字符串的函数。
          (start:number):string;
          interval:number;
          reset():void;
      }
      
      // 定义一个函数,该函数会返回一个函数,并且该返回的函数会实现Counter接口
      // 一个对象可以同时做为函数和对象使用,并带有额外的属性
      function getCounter(): Counter {
          let counter = <Counter>function (start:number) { };
          counter.interval = 123;
          counter.reset = function () { };
          return counter;
      }
      
      let c1 = getCounter();   
      c1(10);
      c1.reset();
      c1.interval = 5.0;
      

接口继承和实现

只要写在接口中的属性,在实现接口的时候,属性都是必须的,可多不可少

  • 确保一致性:通过实现接口,可以确保类包含接口定义的所有必要的属性和方法,这有助于维护代码的一致性和可靠性。
  • 允许重写:类可以实现接口的属性和方法,并可以根据需要对这些属性和方法进行重写或扩展,以添加新的功能。
  • 类型检查:TypeScript 的静态类型检查会确保类正确实现了接口,如果未能正确实现,将会触发编译时错误。
  • 多态性:实现接口的类可以在任何期望该接口的地方使用,这为代码提供了更大的灵活性和可复用性。
继承

类似于类的继承,只不过接口是对类型的继承,可以继承其他接口的属性类型

interface Person {
  name: string;
}
interface User1 extends Person {
  age: number;
}
class U {
  constructor(public name: string, public age: number) {}
}

const user: User1 = new U("11", 222);
console.log(user);
实现

在实现接口的的时候 需要安装接口定义的属性对齐进行真实实现

// 实现ClockInterface接口的Clock类
class Clock implements ClockInterface {
  currentTime: Date;

  constructor(hour: number, minute: number) {
    this.currentTime = new Date();
    this.adjustTime(hour, minute);
  }

  setTime(d: Date): Date {
    this.currentTime = d;
    return this.currentTime;
  }

  private adjustTime(hour: number, minute: number): void {
    this.currentTime.setHours(hour);
    this.currentTime.setMinutes(minute);
  }
}

implements与extends的区别:

  • implements关键字与extends关键字不同,extends用于类的继承,而implements用于实现接口。
  • 虽然它们通常分开使用,但在TypeScript中,一个类可以同时继承一个父类并实现多个接口。

接口合并

  • 接口的非函数的成员应该是唯一的。如果它们不是唯一的,那么它们必须是相同的类型。如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。
  • 对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。 同时需要注意,当接口 A与后来的接口 A合并时,后面的接口具有更高的优先级。
  • interface 任意key 也就是索引签名
    • 在不知道其他数据的情况下,可以使用索引签名的形式对非指明的数据不再做强校验,value的类型会影响已定义的类型,最好是any
// 接口重载
interface Box {
    height: number;
    width: number;
}
interface Box {
    scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10};

// 具有签名的接口 
// 通过传入的参数区分类型
interface Document {
    createElement(tagName: any): Element;
}
interface Document {
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
    createElement(tagName: string): HTMLElement;
    createElement(tagName: "canvas"): HTMLCanvasElement;
}

面向对象中接口的含义

ts对类的一些更新

  • ts中普通类实现

    • 实现方式跟es6差不多,增加一些特殊操作

    • class Animal {
        x: number = 0;
        y: number = 0;
       	public move(distanceInMeters: number = 0) {
         	console.log(`Animal moved ${distanceInMeters}m.`);
       	}
      }
      
  • 继承

    • 可以描述类与类之间的关系

    • 继承之后会拥有父类的所有成员

    • class Dog extends Animal {
       	public bark() {
         	console.log("Woof! Woof!");
       	}
      }
      const dog = new Dog()
      dog.x  // 0
      dog.y  // 0
      
  • 成员的重写

    • 子类中覆盖父类的成员,但是类型无法覆盖,类型必须一致

      • super 关键字

        • 在子类的方法中可以使用super读取成员
      • class Cat extends Animal {
           	x: number = 20;
          	y: number = 20;
        }
        const cat = new Cat()
        cat.x  // 20
        cat.y  // 20
        
    • 注意this关键字

      • 在继承关系中,this的指向是动态的,调用方法时根据具体的调用者确定 this 指向
  • 类型匹配

    • 子类的对象始终可以赋值给父类

    • export class Tank {
        x: number | string = 0;
        y: number = 0;
      }
      export class PlayerTank extends Tank {}
      export class EnemyTank extends Tank {}
      // 使用父类做为实例的类型
      const p: Tank = new PlayerTank();
      
  • 访问修饰符

    • protected

      • 受保护的属性,仅对声明它们的类的子类可见,可以在子类中访问他,不能实例对象中访问

      • 可以被子类重写但是不能被实例读取

      • class Tank {
          protected name: string = "坦克";
        }
        class EnemyTank extends Tank {
          // 可以在子类中重写 
          name: string = "敌方坦克";
          say() {
            // 可以在子类中读取
            console.log(this.name);
          }
        }
        const tank = new Tank();
        // 报错 不能直接在实例使用
        tank.name;
        
    • private

      • 私有属性,只能类的内部使用this访问

      • 可以被实例化,但是实例不能直接使用该属性(编译后也是放到原型上,但是ts做了识别)

      • 可以被继承,但是子类不能再次重写该属性

      • class MyClass {
          private myProperty: string;
          constructor() {
            this.myProperty = "Hello, private!";
          }
          public getMyProperty(): string {
            return this.myProperty;
          }
        }
        // 报错 不能在子类重写private 属性
        class MyClass1 extends MyClass {
          myProperty: string = '2'
        }
        const obj = new MyClass();
        // 报错 不能在实例使用private属性
        console.log(obj.myProperty);
        
    • public

      • 公开属性,可以被实例读取,且会可以被继承,属性会放在实例的原型上
  • 针对constructer的改动(依赖注入)

    • 在使用修饰符定义入参时,在实例化触发的时候会自动进行this属性的赋值

    • class User {
        // 自动挂载到实例上
        constructor(private _name: string,public _age:number,protected _sex:string) {}
        get title() {
          return this._name;
        }
      }
      const user = new User("1");
      console.log(user.title);
      
  • 单根性和传递性

    • 单根性

      • 每个类最多只能拥有一个父类
    • 传递性

      • 如果 A 是B的父类,并且B是C的父类,则可以认为A也是C的父类
  • 索引签名

    • 类也可以声明索引签名,其工作方式也对象一致,其作用是规定实例的索引签名

    • class Person {
        // 定义索引签名
        [s: string]: unknown;
        check(s: string) {
          return this[s];
        }
      }
      const user = new Person()
      
  • 在ts中的访问器属性

    • ts中访问器属性大致和es6一致,但是增加了一些改动

      • 只写了set没有写get 此时会被转换为 只读属性

      • export class Square {
          private _color!: string;
          public get color() {
            return this._color;
          }
        }
        
        const sq = new Square();
        sq.color = '2' // 报错
        
  • 在ts中如何约束一个变量为构造函数

    • Function

      • // 不能当做构造函数
        function test(handler:Function) {}
        
    • new () => object

      • function test(handler: new () => object) {
          return new handler();
        }
        
      • 通用构造函数参数约束

        • function test(handler: new (...argus: any[]) => object) {}
          

类的示例

  • 把类当做接口使用

    • class Point {
        x: number = 0;
        y: number = 0;
      }
      interface Point3d extends Point {
        z: number;
      }
      let point3d: Point3d = { x: 1, y: 2, z: 3 };
      
  • 定义类的类型

    • // 使用鸭子填充法 属性可以多 但是不能少
      class Person {
        loginI!: string;
      }
      type user = new (agu1: any, agu2: any) => Person;
      type ins = InstanceType<user>;
      const A: ins = class User {
        loginI!: string;
        constructor(props1: any, props2: any) {}
      };
      
  • 指定实例为构造函数的结果

    • let greeter: Greeter;
      greeter = new Greeter("Hello, world!");
      greeter.great(); // 输出 "Hello, world!"
      
  • 类实现接口

    • 根据接口实现指定的成员

    • // 描述一个类应该包含的属性和方法
      interface ClockInterface {
        currentTime: Date;
        setTime(d: Date): Date;
      }
      // 根据接口实现指定类属性
      class Clock implements ClockInterface {
        currentTime!: Date;
        setTime(d: Date): Date {
          this.currentTime = d;
          return d;
        }
      }
      
  • 泛型类

    • class GenericNumber<T> {
        zeroValue: T;
        add: (x: T, y: T) => T;
      }
      let myGenericNumber = new GenericNumber<number>();
      myGenericNumber.zeroValue = 0;
      myGenericNumber.add = function (x, y) {
        return x + y;
      };
      //泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
      console.log(stringNumeric.add(stringNumeric.zeroValue, "test")); //输出
      
  • 泛型约束

    • // 泛型约束
      interface Lengthwise {
        length: number;
      }
      //  增加类型继承 可以增加泛型的属性
      function loggingIdentity<T extends Lengthwise>(arg: T): T {
        console.log(arg.length); // 这里会报错,因为泛型T上没有length属性
        return arg;
      }
      // 现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
      // loggingIdentity(7); // 报错 需要包含length属性
      loggingIdentity({ length: 10, value: 3 });
      
  • 多态的 this类型

    • // 多态的 this类型
      // 直接返回this类型
      class BasicCalculator {
        public constructor(protected value: number = 0) {}
        public currentValue(): number {
          return this.value;
        }
        public add(operand: number): this {
          this.value += operand;
          return this;
        }
        public multiply(operand: number): this {
          this.value *= operand;
          return this;
        }
      }
      
      let v = new BasicCalculator(2).multiply(5).add(1).currentValue();
      
      class ScientificCalculator extends BasicCalculator {
        public constructor(value = 0) {
          super(value);
        }
        public sin() {
          this.value = Math.sin(this.value);
          return this;
        }
      }
      
      let v2 = new ScientificCalculator(2).multiply(5).sin().add(1).currentValue();
      

抽象类

  • 为什么需要抽象类

    • 有时某个类,只表示某个抽象概念,主要是用于提取子类共有的成员,而不能直接创建他的对象,该类可以作为抽象类

    • abstract 标记一个类,表示一个类是一个抽象类,该类不能进行实例化

    • 主要用于其他子类继承它

    • abstract class Chess {}
      
  • 抽象成员

    • 父类中可能知道某些成员是必须存在的,但是不知道该成员的值或者实现是什么,因此需要有一种强约束让继承该类的子类必须实现该成员
      • 理解就是在抽象类定义子类需要带那些属性或者方法,为子类定义一个模板
      • 模板是不知道具体的属性或者方法是怎么实现的,只有到具体的子类才知道怎么实现
      • 在属性前面 加上关键字 abstract 表示属性也是抽象的(变为抽象成员),在普通类是不能使用的
    • 抽象成员必须要在子类中实现,否则不能完成继承
      • 不实现 抽象 属性 就会报错
    • 抽象方法
      • 在抽象类中定义抽象只需要写函数名,参数和返回值,不需要写函数体
abstract class Chess {
  x: number = 0;
  y: number = 0;
  // 定义抽象属性
  abstract readonly name: string;
  // 定义抽象方法
  abstract move(targetX: number, targetY: number): boolean;
}
// 直接实现访问器属性
class Horse extends Chess {
  readonly name: string = "马";
  move(targetX: number, targetY: number): boolean {
    this.x = targetX;
    this.y = targetY;
    return true;
  }
}
// 使用访问器属性实现抽象属性
// 没写set 访问器,该属性就是只读
class Pao extends Chess {
  get name() {
    return "炮";
  }
  move(targetX: number, targetY: number): boolean {
    this.x = targetX;
    this.y = targetY;
    return true;
  }
}
// 构造函数里面实现抽象属性
class Soldier extends Chess {
  readonly name: string;
  constructor() {
    super();
    this.name = "兵";
  }
  move(targetX: number, targetY: number): boolean {
    this.x = targetX;
    this.y = targetY;
    return true;
  }
} 

设计模式之模板模式

  • 有些方法所有的子类实现的流程完全一致,只是流程中的某个步骤具体实现不一致
  • 可以将该方法提取到父类,可以在父类中完成整个流程的实现,遇到实现不一致的方法时,将该方法做成抽象方法
  • 规范子类需要实现的步骤,提取通用实现步骤到父类
abstract class Chess {
  x: number = 0;
  y: number = 0;
  abstract readonly name: string;
  // 在父类实现move过程,子类需要自定义规则
  move(targetX: number, targetY: number): boolean {
    if (this.rule(targetX, targetY)) {
      this.x = targetX;
      this.y = targetY;
      console.log("移动成功", this.name);
      return true;
    }
    return false;
  }
  // 定义子类需要实现的规则抽象方法
  abstract rule(targetX: number, targetY: number): boolean;
}

静态成员

  • 附属在构造函数上的成员,直接通过类调用

    • Math.max() // max 就是Math类的静态成员
      
  • 创建静态成员

    • 在类里面使用 static 关键字标记成员

    • 静态成员不会被实例使用和继承,

    • // 在js里面写法
      function Person() {
      }
      Person.login = function () {
        
      }
      
      // ts写法
      class Person{
        static login() {}
      }
      
  • 静态方法中的this

    • 已知实例方法中的this指向的是 当前对象
    • 而静态方法中的this指向的 当前这个类

设计模式之单例模式

某些类的对象在系统中最多只能有一个,为了避免开发者造成随意创建多个对象的错误,可以使用单例模式进行强约束

  • 前置条件
    • 私有化构造函数,不允许外部使用new 关键字
    • 创建静态成员保存实例,暴露给外面唯一实例
  • 写法一
    • 暴露一个静态方法,这个静态方法内部去做判断是否已经初始化
      • 如果已经初始化就返回保存的实例
      • 没有初始化过就进行初始化并且更新,后续获取的都是该实例
    • 外部创建实例需要调用静态方法,多次创建的实例都是同一个
  • 写法二
    • 暴露静态成员,并设置为只读属性,在构造函数第一次时就实例化完成
    • 后续不会再实例化,外部读取这个静态属性即可
  • 两种写法的区别是第二种是直接创建,而不是用到才创建
// 写法一 通过静态方法内部判断
class Board {
  width: number = 500;
  height: number = 700;
  init() {
    console.log("初始化棋盘");
  }
  private constructor() {}
  private static _board?: Board;

  static createBoard(): Board {
    if (this._board) {
      return this._board;
    }
    this._board = new Board();
    return this._board;
  }
}

const b = Board.createBoard();
const b1 = Board.createBoard();
console.log(b == b1)
b.init();
// 写法二
class Board {
  width: number = 500;
  height: number = 700;
  init() {
    console.log("初始化棋盘");
  }
  private constructor() {}
  static readonly singleBoard: Board = new Board();
}

const b = Board.singleBoard;
const b1 = Board.singleBoard;
console.log(b == b1);
b.init();

函数

函数定义 出入参类型约束

  • function add(x: number, y: number): number {
      return x + y;
    }
    

函数签名

  • 通过给函数执行完整的出入参类型 就叫做函数签名

  • //普通写法
    function add(x: number, y: number): number {
      return x + y;
    }
    
    //表达式写法
    // 格式 let myAdd: (x: number, y: number) => number { 函数体 } 
    //  其中 (x: number, y: number) => number 表示函数入参和返回值类型
    let myAdd: (x: number, y: number) => number = function (
      x: number,
      y: number
    ): number {
      return x + y;
    };
    
    // 等于以下写法
    type func = (x: number, y: number) => number;
    let myAdd: func = function (x: number, y: number): number {
      return x + y;
    };
    

函数重载

  • 函数项名称相同,但输入输出类型或个数不同的函数可以被称为函数重载

  • 通过给函数执行不同情况下的出入参,但是不写具体的函数体,即对该函数实现重载

  • 目的是为了在ts执行函数的进行对应的类型推断

  • const users = [1, 2, 3, 4, 5];
    
    function findNum(): number[]; //不传即查所有
    function findNum(ids: number[]): number[]; //传入数组插入对应的数据
    function findNum(id: number): number[]; // 只传一个查对应的详情
    // 最终函数实现
    function findNum(ids?: number | number[]): number[] {
      if (typeof ids === "number") {
        return users.filter((el) => el === ids);
      } else if (Array.isArray(ids)) {
        users.push(...ids);
        return users;
      } else {
        return users;
      }
    }
    

其他

  • 函数参数可选

    • 函数参数可选和默认值只能使用一个

    • function buildName(firstName: string, lastName?: string) {
        if (lastName) return firstName + " " + lastName;
        else return firstName;
      }
      
      let result1 = buildName("Bob"); // works correctly now, returns "Bob"
      // let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
      let result3 = buildName("Bob", "Adams"); // ah, just right
      
  • 剩余参数

    • function buildName1(firstName: string, ...restOfName: string[]) {
        return firstName + " " + restOfName.join(" ");
      }
      let employeeName = buildName1("Joseph", "Samuel", "Lucas", "MacKinzie");
      let buildNameFn: (fname: string, ...rest: string[]) => string = buildName1;
      
  • 对象类型给this增加定义 对象增强 ts独有

    • // 在对象里面的方法的第一参数可以指定this的类型,传参无需考虑该参数
      interface Obj {
        user: number[];
        add: (this: Obj, num: number) => void;
      }
      let obj: Obj = {
        user: [1, 2, 3],
        add: function (this: Obj, num: number) {
          this.user.push(num);
        },
      };
      
      obj.add(4);	
      
  • 函数参数使用泛型

    • // T K是T的value或者key
      function getProperty<T, K extends keyof T>(obj: T, key: K) {
        return obj[key];
      }
      let x = { a: 1, b: 2, c: 3, d: 4 };
      getProperty(x, "a"); // okay
      // getProperty(x, "m"); 
      // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
      
  • ts提供的内置对象

    • //   IArguments 专门用于arguments 实现就是用interface 实现了这个对象的几个属性
      function a(...args: string[]) {
        //   let a: IArguments = arguments;
        let a: a = arguments;
      }
      interface a {
        callee: Function;
        length: number;
        [index: number]: any;
      }
      a("1", "2");
      
  • 函数参数是构造函数

    • class User {
        loginId: string;
      }
      // 定义构造函数 new () => User
      function createUser(cls: new () => User): User {
        return new cls();
      }
      // 定义构造函数 typeof User ,获取类的构造函数
      function createUser1(cls: typeof User): User {
        return new cls();
      }
      const u = createUser(User);
      

ts中this类型约束

  • 在对象里面定义方式时,内部使用this 无法获得准确的this类型

    • 字面量对象可以自动推导出类型
  • 在使用类创建成员属性时,this可以准确推导出this的类型

  • 在ts中允许在书写函数时手动声明该函数的this指向,将this 做为函数的第一个参数

    • 字面量对象写法

    • interface IUser1 {
        name: string;
        age: number;
        sayHello(this: IUser1): void;
      }
      const u1: IUser1 = {
        name: "11",
        age: 22,
        sayHello() {
          console.log(this.age);
        },
      };
      const d1 = u1.sayHello
      d1() // ts检查报错
      
    • 类的写法

    • class User1 {
        public name: string = "2";
        sayHello(this: User1) {
          console.log(this.name);
        }
      }
      
  • 并不是实际的参数,在编译结果里面不会出现

泛型

  • 可以是任意类型, 一旦接收的类型确定后后续使用该泛型就是固定类型
  • 泛型和any的区别在于泛型由调用者指定类型, any由编译器指定类型
  • 泛型可以指定多个,并且存在类型检查,而any则绕过了类型检查
function echo<T>(data: T):void{}
function identity<T>(data: T): T {
    return data;
}
// let myIdentity: <U>(data: U) => U = identity;
let myIdentity: { <T>(data: T): T } = identity;
// 对象属性合并
function extend<T, U>(first: T, second: U): T & U {
  let result = <T & U>{};
  for (let id in first) {
    (result as T)[id] = first[id];
  }
  for (let id in second) {
    (result as U)[id] = second[id];
  }
  return result;
}
class Person {
  constructor(public name: string) {}
}
interface Loggable {
  log(): void;
}
class ConsoleLogger implements Loggable {
  log() {
    // ...
  }
}
// 传入对象做为泛型,合并对象属性
var jim = extend(new Person("Jim"), new ConsoleLogger());
jim.name;
jim.log();

类型兼容性

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
// 类型多的可以兼容类型少的,少的不能反向兼容
y = x; // OK
// x = y; // Error
let x1 = () => ({ name: 'Alice'})
let y1 = () => ({ name: 'Alice', location: 'Seattle' })
x1 = y1; // OK
// y1 = x1; // Error   
x1 == y1; // Error
// 类比较
class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
}
class Size {
    feet: number;
    constructor(numFeet: number) { }
}
let a: Animal;
let s: Size;
// a = s;  // OK
// s = a   

类型关键字

  • typeof

    • 与js中的typeof 类似, 用于获取变量或对象的类型。

    • 在TypeScript中,typeof可以用于类型查询操作符,以获取某个表达式的类型。

    • 当typeof 作用于类的时候,得到的类型是该类的的构造函数

    • // Prints "string"
      console.log(typeof "Hello world");
      const a = 'adsda'
      // 获取a的类型做为一个新的类型
      // a现在是字面量类型  b的类型 必须是a
      const b: typeof a = 'adsda'
      const c : sting = 'cccc'
      // 有声明类型 ,typeof 获取到的就是其声明的类型
      const d: typeof c = 'aaa'
      
  • keyof

    • 用于获取对象类型的键名称列表。

    • 用于获取其他类型中的所有成员类型组成的联合类型

    • type Point = { x: number; y: number };
      type P = keyof Point;
      // p => type P = 'x' | 'y'
      const c: P = "x";
      
  • in

    • 在TypeScript中,in关键字通常用于遍历对象的属性。

    • 此外,它也可以判断对象是否具有某个属性。

    • const obj = { a: 123 };
      if ("a" in obj) console.log("found a");
      
      type keys = "a" | "b" | "c";
      type Flags = { [K in keys]?: boolean };
      
    • // 使用 in 和 keyof 获取对象属性
      type Person = {
        name: string;
        age: number;
      }; 
      // 将person的所有属性值类型变成 boolean类型,得到一个新类型
      // 此时的P 相当于挨个从Person 的key里面取
      type PersonPropertiesToBoolean = {
        [P in keyof Person]: boolean;
      };
      // 等同于
      type PersonPropertiesToBoolean = {
        name: boolean;
        age: boolean;
      };
      
    • // value 的类型 也使用Person 对应的key 的value 的类型
      type PersonPropertiesToBoolean = {
        [P in keyof Person]: Person[P]; 
      };
      
    • // 控制对象属性为只读属性,禁止修改
      type objReadyOnly = {
        readonly [key in keyof User]: User[key];
      }
      
    • // 控制属性为可选
      type objPartial = {
        [key in keyof User]?: User[key];
      }
      
    • // 使用泛型,读取泛型属性
      type UserPartial<T> = {
        [P in keyof T]: T[P];
      };
      
  • extends

    • 用于表示一个类型是另一个类型的子类型。
    • 在接口和类的定义中使用,表示继承关系。
  • as

    • 用于进行类型断言,即告诉编译器我们明确知道某个值的具体类型。

    • 这在处理泛型或类型不确定的情况下非常有用。

    • obj.name as string  // 类型推导为string
      
  • is

    • 用于类型保护,通常用在函数中对参数类型进行判断,以确保参数符合特定的类型约束。

    • // pet 是入参  
      // 返回参数由表达式结果确定   需要确定pet 类型 是 Fish
      function isFish(pet: Fish | Bird): pet is Fish {
        return (pet as Fish).swim !== undefined;
      }
      
  • unknown

    • 表示任何类型的安全版本,可以用来表示我们不确定的值的类型。
    • 与any不同,unknown类型的变量在被使用之前需要进行显式的类型检查或类型断言。
  • infer

    • 关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数

    • 主要用于泛型和高级类型别名的创建。

    • 它通常跟条件运算符一起使用,用在extends关键字后面的父类型之中。

    • // 定一个类型别名 使用Type 做为泛型
      // 自动推导出Item的类型
      type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
      
      // 传入的类型是数组类型 , 那么 Item就是 数组的元素类型
      type str = Flatten<string[]>;
      
      // 传入的不是数组中类型, 就使用显示传入
      type num = Flatten<number>;
      
  • ?

    • 用于表示可选类型,即某个属性或参数可能是undefined或null。

    • interface Person {
        name?:string
      }
      
  • !

    • 用于表示非空断言,即某个值一定不会是null或undefined。

    • const div = document.getElementById("div")!;
      div.style.height = "200px";
      //-------
      function fixed(name: string | null): string {
        function postfix(epithet: string) {
          // 这里使用类型断言,将name断言为string
          // 因为这里我们确定name不是null
          return name!.charAt(0) + epithet;
        }
        name = name || "Bob";
        return postfix(" the " + name.toUpperCase());
      }
      
  • extends...?

    • 条件运算符

    • JavaScript 的?:运算符这样的三元运算符,但多出了一个extends关键字。

    • T extends U ? X : Y
      // 上面式子中的extends用来判断,类型T是否可以赋值给类型U,即T是否为U的子类型,这里的T和U可以是任意类型。
      // 如果T能够赋值给类型U,表达式的结果为类型X,否则结果为类型Y。
      

类型工具(类型演算)

ts内置的一些类型演算工具,在编译之后就会消失

  • Omit

    • 用于从一个类型中剔除某些属性,生成一个新的类型。

    • type Foo = {
      	name: string
      	age: number
      }
      type Bar = Omit<Foo, 'age'>
      // 相当于
      type Bar = {
      	name: string
      }
      
  • Pick

    • 用于从给定类型中选取部分属性构造新类型。

    • interface Foo {
          name: string
          age?: number
          gender: string
      }
      type Bar = Pick<Foo, 'age' | 'gender'>
      // 相当于
      type Bar = {
          age?: string
          gender: string
      }
      
  • Partial

    • 将类型的所有属性变为可选,即每个属性都是非必须的。

    • // Partial<T>
      interface Foo {
          name: string
          age: number
      }
      type Bar = Partial<Foo>
      // 相当于
      type Bar = {
          name?: string
          age?: string
      }
      
  • Required

    • 与Partial相反,它将类型的所有属性变为必选。

    • // Required<T>
      interface Foo {
        name: string;
        age?: number;
      }
      type Bar = Required<Foo>;
      // 相当于
      type Bar = {
        name: string;
        age: string;
      };
      
  • Exclude

    • 排除类型,用于从联合类型中排除某些类型。

    • 从 T 中剔除可以赋值给 U 的类型

    • // Exclude<T,U>
      type sex = "男" | "女" | null | undefined;
      // 排除 null 和  undefined
      type user = Exclude<sex, null | undefined>;
      // user => '男' | '女'
      
  • Extract

    • 合并类型,用于从联合类型进行提取公用类型

    • 提取 T 中可以赋值赋值给U的类型

    • // Extract<T,U>
      type sex = "男" | "女" | null | undefined;
      // 只保留 '女' 和 '男' 
      type user = Extract<sex, "女" | "男">;
      // user => '男' | '女'
      
  • Readonly

    • 将类型 T 中成员变为只读

    • // Readonly<T>
      interface Foo {
        name: string;
        age: number;
      }
      type Foo1 = Readonly<Foo>;
      const obj: Foo1 = {
        name: "1",
        age: 111,
      };
      
      obj.age = 22;  // 提示报错 不能被修改
      
  • NonNullable

    • 从T中剔除null和undefined

    • // NonNullable<T>
      type sex = "男" | "女" | null | undefined;
      type user = NonNullable<sex>;
      
  • ReturnType

    • 获取函数返回值类型

    • // ReturnType<T>  
      // T 表示函数的返回类型
      function getStr(): string {
        return "1";
      }
      type str = ReturnType<typeof getStr>;
      // str => string
      
  • InstanceType

    • 获取构造函数类型的实例类型

    • // InstanceType<T>
      class Person {
        name: string = "2";
      }
      
      type user = InstanceType<typeof Person>;
      const obj: user = {
        name: "22",
      };
      

类型保护

  • 因为ts 不能对运行时态进行检查,所以需要通过使用类型保护机制告诉ts当前变量的类型以及成员属性
    • 通过 关键字 instanceof 或者 in typeof 判断得到更加准确的类型

      • TypeScript 将会推导出在条件块中的的变量类型
    • 通过函数进行判断,ts就能识别交叉类型

class Person {
  name: string;
}
class Anima {
  type: string;
}
function getType(p) {
  if (p instanceof Anima) {
    p.type = "2";
  }
  if (p instanceof Person) {
    p.name = "";
  }
}
// 实现类型保护函数
// 接受一个参数 类型定义为对象 ,  函数返回类型为boolean  ,通过is 标识符告诉ts是否是该接口的子类
// 返回true表示该对象 具有 IFireShow 的成员属性
function hasFireShow(ani: object): ani is IFireShow {
  if (
    (ani as unknown as IFireShow)?.singleFire &&
    (ani as unknown as IFireShow)?.doubleFire
  ) {
    return true;
  }
  return false;
}
animals.forEach((a) => {
  // 调用类型保护函数 内部调用方法不会出现错误
  if (hasFireShow(a)) {
    a.singleFire();
    a.doubleFire();
  }
});

索引器

  • 使用对象的成员 对象[值] ,也就是成员表达式

    • // 在js里面使用
      const obj = {
        name: "abc",
        age: 22,
      };
      console.log(obj["age"]);
      
    • // 在es6 新增支持 成员属性使用变量
      const name = "222";
      class Person {
        [name] = "222";
        [name + "hello"]() {}
      }
      const user = new Person();
      // 使用成员属性
      user[name];
      
  • 在ts新增对成员索引的类型检查

    • ts对使用对象的成员属性的时候会检查对象里面是否存在该属性值

    • 由于ts是不会参与运算,一些可能没有新增属性是拿不到对应的属性名,所以就会使用any类型

    • 需要开启严格模式 或者 noImplicitAny 配置

    • class User1 {
        constructor(public name: string, public age: number) {}
      }
      const u1 = new User1("11", 22);
      u1["loginid"] = "2";  // 提示  类型“User1”上不存在属性“loginid”
      console.log(u1["loginid"]);
      
  • 使用索引器指定成员类型

    • 指定索引器后新增的属性,在读取不存在的属性的时候就不会有隐式any

    • class User1 {
        // 属性名 类型为 string
        // 属性值 类型可以是 any
        [props: string]: any;
        constructor(public name: string, public age: number) {}
      }
      
  • 在索引器中,键的类型可以是字符串也可以是数字(二选一)

    • class MyArray {
        [key: number]: any;
        0 = 1;
        1 = 2;
        2 = 4;
      }
      const arr = new MyArray();
      arr[5] = 6;
      
  • 在类的里面定义索引器是要写在类的最顶端

  • 索引器使用多个类型

    • 但是value的类型必须一致,或者是子类和父类关系

    • class MyArray {
        [key: number]: any;
        [key: string]: any;
        0 = 1;
        "a" = 3;
      }
      
    • class A {
        name = "2";
      }
      class MyArray {
        [key: number]: A;
        [key: string]: Object;
      }
      const arr = new MyArray();
      arr[5] = { name: "11" };
      

高级类型

  • 联合类型
    • 联合类型表示一个值可以是几种类型之一。

    • // string | number 表示一个值可以是字符串或数字。
      const a :  string | number = '111'
      
  • 交叉类型
    • 交叉类型表示一个值必须同时满足多种类型。

    • // 表示一个值必须是字符串并且拥有length属性。
      const a :  string & { length: number } = '111'
      
  • 映射类型
    • 映射类型允许你基于已有的类型创建新的类型,可以看作是类型的转换。

    • type User = {
        name: string;
        location: string;
        age: number;
      }
      // 通过关键字 把原始的类型变为可选类型
      type User2 = Partial<User>;
      

类型别名

类型别名用来给一个类型起个新名字。这可以用来使代码更加清晰,或者创建更复杂的类型。

//  类型别名
type Name = string;
type NameResolver = () => string;
// 定义联合类型
type NameOrResolver = Name | NameResolver;
// 使用合并后的组合类型作为信息的类型
function getName(n: NameOrResolver): Name {
  if (typeof n === "string") {
    return n;
  } else {
    return n();
  }
}
// 定义泛型
type Container<T> = { value: T };

// 我们也可以使用类型别名来在属性里引用自己:
type Tree<T> = {
  value: T;
  left: Tree<T> | null;
  right: Tree<T> | null;
};
// 交叉类型加类型别名
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
  name: string;
}
var people: LinkedList<Person> = <LinkedList<Person>>{};
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
// 字符串字面量类型
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
  animate(dx: number, dy: number, easing: Easing) {
    if (easing === "ease-in") {
    } else if (easing === "ease-out") {
    } else if (easing === "ease-in-out") {
    }
  }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
// button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
// 字符串字面量类型还可以用于区分函数重载:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
function createElement(tagName: string): Element { }

接口和类型别名区别

  • type 是创建类型别名,interface 是创建接口
  • interface 能够实现继承,type 不行
  • type只能通过 & 进行交叉,interface可以实现继承
// 对比type 和  interface 使用
type Alias = { num: number };
interface Interface {
  num: number;
}
// 索引类型和字符串索引签名
// keyof和 T[K]与字符串索引签名进行交互

interface Map<T> {
  [key: string]: T;
}

let keys: keyof Map<number>; // string
let value: Map<number>["foo"]; // number
// 映射类型
interface PersonPartial {
  name?: string;
  age?: number;
}

interface ReadonlyPersonPartial {
  readonly name?: string;
  readonly age?: number;
}

type ReadonlyKeys<T> = {
  readonly [P in keyof T]: T[P];
};

type PartialKeys<T> = {
  [key in keyof T]?: T[key];
};

// 动态创建可选属性
type PersonPartial1 = PartialKeys<Person>;
// 动态创建只读属性
type ReadonlyPersonPartial1 = ReadonlyKeys<Person>;
// 代理
function handle() {}
type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

// 由映射类型进行推断
function unProxify<T>(t: Proxify<T>): T {
    let result = <T>{};
    for (let k in t) {
        result[k] = t[k].get();
    }
    return result;
}

内置对象

// ecma 内置对象类型就是构造器类型
let number: Number = new Number(10);
let date: Date = new Date();
let reg: RegExp = new RegExp("abc");
let error: Error = new Error("error");
let xhr: XMLHttpRequest = new XMLHttpRequest();

// dom
// HTMLDivElement => HTML(元素名称)Element  HTMLElement  Element
let div = document.querySelector("footer") as Element;
let nodes: NodeList = document.querySelectorAll("footer");
let divs: NodeListOf<HTMLDivElement | HTMLElement> =
  document.querySelectorAll("div footer");

// bom
let local: Storage = localStorage;
let lo: Location = location;
let cookie: string = document.cookie;
let promise: Promise<number> = new Promise((resolve) => {
  resolve(1);
});
promise.then((res) => {
  console.log(res);
})

声明文件

  • 什么是声明文件 ?

    • d.ts 结尾的文件
  • 声明文件有什么作用?

    • 在js 和 ts 混用的时候,ts 读不到 js的声明信息,就可以通过 d.ts 文件来编写类型声明
    • 一些js或者node 内置模块,也是通过提前安装对应的d.ts文件才能读取到类型声明
      • node_modules下的@typs 目录
    • 总结就是为 js 代码提供类型声明
  • 声明文件的书写位置

    • 放置到 tsconfig.json 配置包含的目录中

      • {
          include:['./src']
        }
        
    • 放置到 node_modules/@types 文件夹中

    • 手动配置

      • 使用该配置后,前面两个配置就失效了

      • {
          typeRoots:['./typs']
        }
        
    • 与js代码所在目录相同,并且文件名也相同的文件

      • - test.js
        - test.d.ts
        

编写声明文件

declare 关键字用于声明全局变量、类型、函数、类、命名空间等,但不实际定义它们的实现。

  • 使用declare的目的是为了提供类型信息,让TypeScript编译器能够在编译时进行类型检查,而不影响最终生成的JavaScript代码

  • 手动编写

    • 对已有库,他是使用js书写而成,并且更新该库的代码为ts的成本较高

    • 全局声明

      • 只是在告诉ts我有这么一个全局对象,跟运行没有关系,实际上没有这个对象还是会报错

      • 声明文件不能写赋值,它只是做类型声明

      • // src/global.d.ts
        // 声明一个全局变量 对外暴露
        declare var Taro: {
          // 对象名简写
          test(props?: any): void;
          data: string;
        };
        // src/index.ts
        // 没有写上面的声明文件就会ts提示错误
        Taro.test() 
        
      • // 使用接口写法
        interface Taro {
          test(props?: any): void;
          data: string;
        }
        declare var Taro: Taro;
        
  • 自动生成

    • 工程使用ts代码开发,发布(编译)之后,是js文件,发布的是js文件
    • 如果发布的文件需要其他开发者使用,可以使用声明文件来描述发布结果中的类型
    • 配置tsconfig.json 中的 declaration
      • 或者使用编译命令 tsc ./src/index.ts -d, -d 的效果就是编译声明文件

命名空间

  • 表示命名空间.可以将其认为是一个对象,有点像块级作用域

  • 命名空间中的内容必须通过命名.成员名访问

  • 在没有模块化之前都是使用命名空间

    • // 声明命名空间
      declare namespace Taro {
        // 声明函数
        function test(props: any): void;
        // 声明变量
        var title: string;
      }
      // 使用声明
      Taro.test()
      

模块

  • 使用第三方模块时有可能对方是js版本,没有类型检查,ts在代码会报错警告
  • 我们可以通过 编写声明文件 给第三方模块编写类型声明
// src/index.ts
import { chunk } from "lodash";
chunk([1, 2, 3], 2);
// lodash.d.ts
//声明模块
declare module "lodash" {
  export function chunk<T>(array: Array<T>, size: number): Array<Array<T>>;
}
/// <reference path="./index.d.ts" />
declare module "lodash" {
  type chunk = <T>(array: Array<T>, size: number) => Array<Array<T>>;
  var _: {
    chunk: chunk;
  };

  export default _;
}

三斜线指令

在一个声明文件中包含另一个声明文件

// 加载其他路径的声明文件
// 只能使用相对路径
/// <reference path="./index.d.ts" />

装饰器

概述

  • 面向对象的概念( java 里面叫 注解 ) (翻译 : decorator)
  • 目前 js 支持装饰器,还没有成为最终的标准

解决的问题

装饰器,能够待来额外的信息,可以达到分离关注点( 定义基础信息,通过装饰器附加额外信息 )

  • 信息书写未知的问题,在定义某个东西时,应该最清楚改东西的情况,不能把规则分离到其他地方
  • 重复代码问题

上述两个问题产生的根源: 某些信息在定义时,附加的信息有限

装饰器的作用

  • 为某些数据(属性,类,参数,方法)提供元数据信息( meta data )

    • 元数据就是描述数据的数据

    • class User {
        //	 定义属性规则 最后通过统一方法进行验证
      	@require   //   该属性表示必填
        @range(3,5) // 取值范围
        name:string; // 实际属性
      }
      

装饰器的本质

  • 装饰器在js中是一个函数 ( 装饰器是要参与运行的 )
  • 装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上
  • 装饰器可以修饰
    • 修饰类
    • 属性 ( 数据 和 方法)
    • 参数

装饰器语法特征

  1. 第一个字符(或者说前缀)是@,后面是一个表达式。
  2. @后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
  3. 这个函数接受所修饰对象的一些相关值作为参数。
  4. 这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
    1. 也叫装饰器工厂
// 最终制作装饰器函数的函数
function creator() {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log("g(): called");
  };
}
// 装饰器函数
function f() {
  console.log("f(): evaluated");
  return creator();
}
function g() {
  console.log("g(): evaluated");
  return creator();
}
class C1 {
  @f()
  @g()
  method() {}
}

类装饰器

  • 类装饰器的本质是一个函数,该函数接收一个参数,表示类本身(构造函数本身),使用装饰器 @函数

  • 在ts 中使用装饰器需要 开启 "experimentalDecorators": true

  • 装饰器在类定义后直接运行

  • 类装饰器可以具有的返回值(只能使用以下两种类型)

    • void: 仅运行函数

    • 返回一个新的类,会将新的类替换掉装饰目标

      • 在内部会丢失类型检查,因为不确定参数来源

      • function test(handler: new () => object) {
          return class B extends handler {};
        }
        
        @test
        class A {}
        const a = new A();
        console.log(a)   // class B
        
  • 如果是调用装饰器的情况,调用结果必须返回一个函数

    • function test(str: string) {
        return function test(handler: new (...argus: any[]) => object) {};
      }
      
      @test("这是一个类型")
      class A {
        constructor(public props: string) {}
      }
      
  • 多个装饰器的执行过程

    • 会按照后加入先执行的顺序执行

    • type constructor = new (...argus: any[]) => object;
      function test(str: string) {
        console.log(str);
        return function test(handler: constructor) {
          console.log(str);
        };
      }
      function test2(str: string) {
        console.log(str);
        return function test(handler: constructor) {
          console.log(str);
        };
      }
      @test("这是一个类型")
      @test("这是一个类型1")
      class A {
        constructor(public props: string) {}
      }
      /*
      	执行结果
      	这是一个类
        这是一个类1
        这是一个类1
        这是一个类
      */
      

属性装饰器

  • 属性装饰器也是一个函数,该函数需要两个参数

    • 参数1
      • 如果是静态属性 则为类本身
      • 如果是实例属性 则为类的原型
    • 参数2
      • 固定为一个字符串,表示属性名
  • 属性装饰器执行顺序也是后入先执行的顺序执行

  • function logClass(target: any, key: any) {
      console.log(target, target == A.prototype);
      console.log(key);
      // 往原型上挂一个属性
      if (!target.__props) {
        target.__props = [];
      }
      target.__props.push(key);
    }
    
    class A {
      @logClass
      public name: String | undefined;
      @logClass
      public: number | undefined;
    }
    const a = new A();
    console.log((a as any).__props);
    console.log((A.prototype as any).__props);
    
  • function logClass(target: any, key: any) {
      // 静态属性时 得到就是 构造函数本身
      console.log(target, target == A);
      console.log(key);
    }
    class A {
      @logClass
      public name: String | undefined;
      @logClass
      static age: number | undefined;
    }
    

方法装饰器

  • 方法装饰器也是一个函数,该函数需要三个参数
    • 参数1
      • 如果是静态方法 则为类本身
      • 如果是实例方法 则为类的原型
    • 参数2
      • 固定位字符串,表示方法名
    • 参数3
      • 属性描述对象
  • 方法装饰器执行顺序也是后入先执行的顺序执行
// 让传入的方法变为可枚举
function enumerable(target: any, key: string, descriptor: PropertyDescriptor) {
  descriptor.enumerable = true;
}
// 传入的方法修改其函数体
function useLess(target: any, key: string, descriptor: PropertyDescriptor) {
  descriptor.value = function () {
    console.log(key + "方法已过期");
  };
}
class A {
  @enumerable
  @useLess
  method1() {
    console.log("method1");
  }
  @enumerable
  method2() {}
}

const a: { [key: string]: any } = new A();

for (const key in a) {
  const element = a[key];
  console.log(key);
}
a.method1();   // method1方法已过期

参数装饰器

参数装饰器一般用于依赖注入,

要求函数有三个参数

  • 如果方法是静态的,则为类本身,如果方法是实例方法,则为类原型
  • 方式名称
  • 在参数列表中的索引
function test(target: any, methodName: string, index: number) {
  console.log(target, methodName, index);
}

class MyMethod {
  sum(a: number, @test b: number) {
    return a + b;
  }
}

自动注入元数据

  • 如果安装了 reflect-metadata ,并且导入了该库,并且在某个成员上添加了元数据,并且启用了 emitEecoratorMetadata ,则ts在编译结果中,会将约束的类型,做为元数据加入到相应位置
  • 一旦有了元数据支持,ts将在运行时将会支持类型检查
import "reflect-metadata";
class User {
  @Reflect.metadata("a", "b")
  loginId!: string;
  @Reflect.metadata("a", "b")
  age!: number;
}

装饰器练习

function classDescriptor(message: string) {
  return function (target: Function) {
    target.prototype.$classDescription = message;
  };
}

//target User 的原型
function propDescriptor(message: string) {
  return function (target: any, key: string) {
    if (!target.$descArr) {
      target.$descArr = [];
    }
    target.$descArr.push({
      propName: key,
      description: message,
    });
  };
}

function printObj(prop: any) {
  if (prop.$classDescription) {
    console.log(prop.$classDescription);
  } else {
    console.log(Object.getPrototypeOf(prop).constructor.name);
  }

  if (!prop.$descArr) {
    prop.$descArr = [];
  }
  for (const key in prop) {
    if (Object.prototype.hasOwnProperty.call(prop, key)) {
      const element = prop[key];
      const _item = prop.$descArr.find((el: any) => el.propName == key);
      console.log(`\t${_item?.description || key} - ${element}`);
    }
  }
}

@classDescriptor("用户")
class User {
  @propDescriptor("账号")
  loginId!: string;
  @propDescriptor("密码")
  loginPwd!: string;
  printObj() {}
}

const u = new User();
u.loginId = "abc";
u.loginPwd = "123";
printObj(u);

AOP 编程方式

  • 属于是一个编程方式,属于面向对象开发
  • 将一些在业务中共同出现的功能块,横向切分,以达到分离关注点的目的

通过装饰器为每个属性配置规则,最终只需要做验证即可,

ts中面向对象

  • 单一职能原则
    • 每个类制作跟它相关的一件事
  • 开闭原则
    • 系统中的类,应该对扩展开放,对修改关闭
  • 使用数据-界面分离模式

传统面向对象语言,书写的类的属性时,往往会进行如下操作

  1. 所有的属性全部私有化
  2. 使用公开的方法提供对属性的访问

实现俄罗斯方块游戏

// Square.ts
export class Square {
  private _viewer?: IViewer;

  // 通过访问器属性暴露内部属性
  get viewer() {
    return this._viewer;
  }

  set viewer(v) {
    this._viewer = v;
  }

  public get point() {
    return this._point;
  }
  public set point(val) {
    this._point = val;
    // 完成显示触发
    if (this._viewer) {
      this._viewer.show();
    }
  }
  public get color() {
    return this._color;
  }
	// 通过private 创建内部属性
  constructor(private _point: Point, private _color: string) {}
}
// SquarePageViewer.ts
import { Square } from "../Square";
import $ from "jquery";
import { IViewer } from "../types";
import PageConfig from "./PageConfig";

/**
 * 显示一个小方块到页面上
 */
export class SquarePageViewer implements IViewer {
  private dom?: JQuery<HTMLElement>;
  private isReMove: boolean = false; // 记录是否已经移除
  constructor(private Square: Square, private container: JQuery<HTMLElement>) {}
  show(): void {
    if (this.isReMove) {
      return;
    }
    if (!this.dom) {
      this.dom = $("<div>")
        .css({
          position: "absolute",
          width: PageConfig.SquareConfig.width,
          height: PageConfig.SquareConfig.height,
          border: "1px solid #ccc",
          boxSizing: "border-box",
        })
        .appendTo(this.container);
    }
    this.dom.css({
      left: this.Square.point.x * PageConfig.SquareConfig.width,
      top: this.Square.point.y * PageConfig.SquareConfig.height,
      background: this.Square.color,
    });
  }
  remove(): void {
    if (this.dom && !this.isReMove) {
      this.dom?.remove();
      this.isReMove = true;
    }
  }
}
// index.ts
import { Square } from "./core/Square";
import { SquarePageViewer } from "./core/viewer/SquarePageViewer";
import $ from "jquery";

const sq = new Square(
  {
    x: 0,
    y: 0,
  },
  "red"
);

sq.viewer = new SquarePageViewer(sq, $("#root"));
sq.color = "red";
sq.point = {
  x: 3,
  y: 0,
};

$("#btnDown").click(function () {
  sq.point = {
    x: sq.point.x,
    y: sq.point.y + 1,
  };
});

$("#btnRemove").click(function () {
  if (sq.viewer) {
    sq.viewer?.remove();
  }
});

$("#btnAdd").click(function () {
  sq.viewer = new SquarePageViewer(sq, $("#root"));
});