1. 泛型有多重要?

  1. Vue3 源码充斥着大量的 TS 泛型。懂 TS 泛型是读懂 Vue3 源码不可逾越的环节。
  2. 前端各种技术的声明文件(d.ts 文件)TS 泛型更是随处可见。例如:小到一个 Array,ES6 的 Set,Map,稍微复杂点的例如:Vue3应用的声明文件,Vuex 底层的声明文件,React 组件声明文件,axios 声明文件,这样的例子举不胜举。
  3. 现在采用 TS 整合前端框架的大中项目越来越多,而 TS 泛型必然成了你必须攻克的核心技能。如果你近几年在公司做过稍微大点的项目,你的感触会特别深刻。
  4. 后端 Nodejs 和 TS 整合的频次也越来越高,优秀的 Nestjs 框架就完全采用 TS 开发。
  5. TS 语法是晋级高级前端工程师,拿更高薪水,面试加分不可逾越的学习环节,而泛型语法更是重重之重,一句我能熟练解说 Vue3 源码中的 TypeScript 语法会为你的面试加分许多。

为什么要使用泛型类?
好处1:编译期对类上调用方法或属性时的泛型类型进行安全检查(类型安全检查),不符合泛型实际参数类型(泛型实参类型) 就编译通不过,防止不符合条件的数据增加进来。
好处2:一种泛型类型被具体化成某种数据类型后,该数据类型的变量获取属性和方法时会有自动提示,这无疑提高代码开发效率和减少出错率。

2. 泛型重构简易版Java的ArrayList

  1. class ArrayList<T = any> {
  2. public element: Array<T>;
  3. public index: number = 0;
  4. constructor() {
  5. this.element = [];
  6. }
  7. public add(ele: T) {
  8. this.checkIndex();
  9. this.element[this.index++] = ele;
  10. }
  11. private checkIndex() {
  12. if (this.index < 0) {
  13. throw new Error("数组下标不能小于零");
  14. }
  15. }
  16. get(index: number): T {
  17. return this.element[index];
  18. }
  19. show() {
  20. this.element.forEach((ele) => {
  21. console.log(ele);
  22. });
  23. }
  24. remove(value: number): number;
  25. remove(value: T): T;
  26. remove(value: number | T): number | T {
  27. this.element = this.element.filter((ele, index) => {
  28. if (typeof value === "number") {
  29. return value !== index;
  30. } else {
  31. return value !== ele;
  32. }
  33. });
  34. return value;
  35. }
  36. }
  37. type studentType = { name: string; age: number };
  38. let student1: studentType = { name: "wangwu", age: 23 };
  39. let student2: studentType = { name: "zhangsan", age: 27 };
  40. let student3: studentType = { name: "lisi", age: 22 };
  41. let arrayList = new ArrayList<studentType>();
  42. arrayList.add(student1);
  43. arrayList.add(student2);
  44. arrayList.add(student3);
  45. console.log(arrayList.get(1));

3. 泛型类定义

泛型是一种参数化的数据类型,具有以下特点的数据类型叫泛型:
特点一:定义时不明确,而使用时必须明确称某种具体类型的数据类型。(泛型的宽泛)
特点二:编译期间进行数据类型安全检查的数据类型。(泛型的严谨)
【特别注意】

  1. 泛型安全检查发生在编译期间。
  2. 泛型是参数化的数据类型,使用时明确后的数据类型就是参数的值。

泛型类的格式
class 类名<泛型形参类型=泛型的默认值>
泛型形参类型有两种表示:一种是A-Z任何一个字母,另一种是语义化的单词来表示。绝大多数情况,泛型都是采用第一种形式表示,如下:

  1. class ArrayList<T=any>{
  2. array: Array<T>
  3. add(data: T) {
  4. ...
  5. }
  6. ...
  7. }

4. object为什么不能替代类上的泛型?

  1. 编译期间object无法进行类型安全检查,而泛型在编译期间可以进行类型安全检查。object接受且只能接受所有的object类型的变量,比如有Customer、Student、Dog类的实例都是对象类型。但如果我们只希望添加Customer类的对象,当添加其他类的对象必须出现编译错误,但object无法做到,只能采用泛型了。
  2. object类型数据无法接受非object类型的变量,只能接受object类型的变量,泛型能轻松做到
  3. object类型数据获取属性和方法时无法自动提示。一种泛型类型被具体化成某种数据类型后,该数据类型的变量获取属性和方法时会有提示,提高代码开发效率和减少出错率,但在object类型的变量无法和获取数据类型的属性和方法,降低了开发效率。

    【Object和object类型的区别】

    1. Object是所有类型的父类,比如let x: Object = "s",变量x可以接受任何数据类型。并且由于是从js继承过来的,所以包含了Object对象的方法和属性,例如toString、hasOwnProperty等等。
    2. unkown也是所有类型的父类,但不包含任何方法和属性。
    3. object只能接受对象类型,并且不包含任何属性和方法。

5. any为什么不能替代类上的泛型?

  1. 编译期间 any 无法进行类型安全检查,而泛型在编译期间可以进行类型安全检查。我们学过:any 是所有类型的父类,也是所有类型的子类。如果我们现在是一个宠物店类,希望只能添加 Dog 类,当调用 add 方法添加 Customer、Student 类必定出现编译错误,从而保证了类型安全检查,但是 any 类型无法保证类型安全检查,可以为任意类型,包括 string,number,boolean,null,undefined,never,void,unknown 基础数据类型和数组,类,接口类型, type 类型的变量全部能接受,不会进行无法进行类型安全检查。
  2. any 类型可以获取任意数据类型的任何属性和任意方法而不会出现编译错误导致潜在错误风险,而泛型却有效的避免了此类问题发生。any 类型可以获取任何属性和任意方法而不会出现编译错误,因为any可以代表任意数据类型来获取任意属性和任意方法,但是泛型类型被具体化成某种数据类型后,该数据类型的变量调用该数据类型之外的属性和方法时,出现编译错误,这也减少了代码隐藏潜在错误的风险。
  3. any 类型数据获取属性和方法时无自动提示,泛型有自动提示。

【注意】any 类型可以代表任意数据类型来获取任何属性和任意方法而不会出现编译错误,因为any可以代表任意数据类型来获取任意属性和任意方法。any 的这个特性是一把双刃剑,当我们需要这么使用,它给我们带来方便,但是大多数情况下我们是不需要这么做的。

6. 通用分页类的实现

1. 分页类的准备:理解DAO

DAO数据访问层:NodeJS或者其他后端语言中的数据访问层。就是很多类的合计,每一个类就是一个DAO。简单来说,DAO层的每一个类一般是后端数据表中一个实体的增删改查方法的封装类。
完成了什么:页面上的某个功能操作需要的数据都来自某个DAO类的一个或者多个方法返回的结果。

DAO层出现的意义

  1. 封装性+见名思意:当我们执行查询只需多次调用find相关的方法,执行删除就调用delete相关的方法……一目了然。
  2. 复用性:复用性表现在方法被重复调用多次,通常页面上多个功能有可能需要调用同一个DAO类的同一个方法,我们举一个简单和复杂的例子
    1. 注册和登录我们都需要调用DAO类的findUser方法。
    2. 权限管理功能中为角色定制权限、根据角色查询权限这两个功能都需要用到角色权限查询的方法。findRightByRoleId。
  3. 分工明确,各司其职,利于维护:每一个DAO类把混杂在其他位置中的代码分离出来,单独成类,后期维修维护都很方便。 :::info DAO层中的类如何命名
    从规范上要求:DAO层中的每一个类都以DAO结尾,常规的操作一般一个DAO方法就能搞定。例如页面显示所有的没事数据来自FoodDAO的findAllFoods方法,而每一个美食的详细数据都来自于FoodDAO的findOne。 :::

    2. T extends object

    :::info T extends object是泛型约束的一种表现。泛型约束简单来说就是把泛型的具体化数据类型范围缩小。 :::

理解**T extends object**
extends表示具体化的约束类型只能是object类型,某个变量如果能断言成object类型,那么这个变量的类型就符合T extends object。就是说该变量的类型可以是T的具体化类型。
还记得new的底层发生了什么?任何一个类或者构造函数的底层都是从new Object()而来,这个new Object()对象的类型就是object类型。这就是说任何类的对象或者构造函数的对象都符合T extends object。

分页类使用T extends object的原因
分页类中只能添加对象数据,所以泛型要被具体化成一个对象类型才符合要求,例如多个顾客对象,多个美食对象,而拒绝添加string、number和其他数据类型。

7. keyof 在vue3源码中的应用

1. 理解keyof

keyof表示获取一个类或者对象类型或者一个接口类型的所有属性名组成的联合类型。

  1. let obj = { address: "南京", phone: 1233, desc: "地址" };
  2. // type myobjtype = typeof obj;
  3. // type keyofobj = keyof myobjtype;
  4. type keyofobjtype = keyof typeof obj;
  5. let keyofobj: keyofobjtype = "address";
  6. // 获取Order类上所有public属性+所有的public方法组成的联合类型
  7. type keyofOrder = keyof Order; // "orderId"|"custname"|...
  8. let allvalue: keyofOrder = "orderId";

2. T extends object + K extends keyof T 组合使用带来的好处

在set方法中,可以限制设置的类型为T[K],限制类型并方便代码提示。

  1. class ObjectImpl<T extends object, K extends keyof T> {
  2. private object!: T;
  3. private key!: K;
  4. constructor(object_: T, key_: K) {
  5. this.object = object_;
  6. this.key = key_;
  7. }
  8. get() {
  9. return this.object[this.key];
  10. }
  11. set(newVal: T[K]) {
  12. this.object[this.key] = newVal;
  13. }
  14. }
  15. let orderDetailOne = new OrderDetail(10, "电视机", 5000, 3);
  16. let orderDetailTwo = new OrderDetail(11, "桌子", 2000, 2);
  17. let orderDate = new Date(2023, 10, 17, 5, 20, 0);
  18. let order = new Order(1, orderDate, "李武", "33333", [
  19. orderDetailOne,
  20. orderDetailTwo,
  21. ]);
  22. let objectImpl = new ObjectImpl<Order, "phone">(order, "phone");
  23. objectImpl.get(); // (method) ObjectImpl<Order, "phone">.get(): string
  24. objectImpl.set("8908980"); // (method) ObjectImpl<Order, "phone">.set(newVal: string): void

8. 泛型接口

:::info 💬ArrayList和LinkedList
ArrayList应用场景:ArrayList、Set底层都是基于数组二次封装的类,所以查询效率很高,但插入、更新、删除的效率低。
LinkedList应用场景:基于链表结构插入、删除数据的效率高,但查询效率低。另外LinkedList提供了addFirst,addLast等更多灵活添加数据的方法。
因此,如果项目需要频繁插入、更新、添加操作,那么就需要使用LinkedList,比如:新闻项目、股票系统。而如果对于查询量大,数据变化小的项目就要用ArrayList,比如人口普查系统。 ::: 使用泛型接口的好处

  1. 降低代码管理成本,提供统一属性和方法命名。
  2. 可以从整体上快速通读类的共同方法和属性。
  3. 新增相同类功能时,可以快速搭建类的方法。
  4. 和多态结合增加了项目的拓展性和简洁渡,对开发大中项目有好处。 ```typescript interface List { add(ele: T): void; get(index: number): T; size(): number; remove(value: T): T; }

class LinkedList implements List { add(ele: T): void { throw new Error(“Method not implemented.”); } get(index: number): T { throw new Error(“Method not implemented.”); } size(): number { throw new Error(“Method not implemented.”); } remove(value: T): T { throw new Error(“Method not implemented.”); } }

  1. <a name="OtR45"></a>
  2. ## 9. 泛型接口+泛型类+泛型约束+多态
  3. 使用以上方法,可以将Customer类的rentVehicle方法拓展成可以租赁任何东西的rent方法。
  4. ```typescript
  5. class Customer {
  6. public rentVechile(myVechiles: List<Vechile>) {
  7. let total: number = 0;
  8. for (let i = 0; i < myVechiles.size(); i++) {
  9. total += myVechiles.get(i).calculateRent();
  10. }
  11. return total;
  12. }
  13. }

拓展后

  1. class Customer {
  2. public rent<T extends object>(itemArray: List<T>) {
  3. let total = 0;
  4. for (let i = 0; i < itemArray.size(); i++) {
  5. let item = itemArray.get(i);
  6. total += (item as any).calculateRent();
  7. }
  8. }
  9. }