1. 课程导学

课程解决的痛点

【痛点】已经掌握大量TS基本语法,但不能应付公司中的大中项目的要求。

  1. 不能多维度掌握TS知识。本课程从Vue3源码、TS底层复杂JS源码、真实应用场景、实战+手写源码入手解决。
  2. 对TS的广度和深度掌握远远不够,理解不透彻,一知半解。本课程涵盖类、泛型、装饰器、深度、泛型达50,兼具深度和广度。
  3. 缺少实战能力,没有足够的实战和原理同步同行,就很难锤炼出高水准的TS。本课程深入+手写TS底层源码、手写Promise源码、手写Vuex源码。

    为什么要学习TS高级课程

    【TS本质】TS是一门融合了部分Java后端思想的前端JS语言。
    需要同时吃透:

  4. TS蕴含的Java核心思想。

  5. TS底层复杂的JS技能。
  6. TS自带语法。
  7. 无比重要和有深度的庞杂的泛型技能。

    预期结果

  8. TS水平达到质的飞跃,称为TS高手。

  9. 能轻松搞定大厂面试中复杂的TS面试题。
  10. 能看懂Vue3源码中各种复杂TS语法和其他类d.ts文件中的TS语法。

    2. 深度透彻掌握原型

    1.为什么要用原型?【好处】

    原型上所有的方法和属性都可以被构造函数(实际开发原型主要共享方法和所有实例公用引用属性)的实例共享,那为什么要共享呢?

    2.没有用原型会有什么问题?

    总结问题:所有QQUser对象(也叫实例)都有相同的共同好友属性commonfriends,所有的QQUser对象都有相同的show方法。但我们发现每一个QQUser对象都单独分配一个commonfriends属性和show方法。导致的大量的空间浪费。

使用原型解决所有实例上的方法,还有所有实例上的共同属性都可以放到原型上定义。

  1. function QQUser(QQNo_, QQAge_, QQMark_) {
  2. this.QQNo = QQNo_;
  3. this.QQAge = QQAge_;
  4. this.QQMark = QQMark_;
  5. this.commonfriends = ['xiaoming', 'ale', 'xiaocao'];
  6. this.show = function () {
  7. console.log(`QQ号:${this.QQNo}, QQ年龄:${this.QQAge}, QQ标注: ${this.QQMark}`);
  8. console.log(`共同的好友:${this.commonfriends}`);
  9. };
  10. }
  11. // 对象也叫实例instance
  12. // QQZhangSan叫做对象变量
  13. // 对象是new出来的实例,运行期间才在堆中开辟对象的内存空间。
  14. let QQZhangSan = new QQUser('zhangsan', 15, '心在跳舞');
  15. let QQLiSi = new QQUser('lisi', 24, 'happy man on the land');
  16. QQZhangSan.show();

第一章 全栈思维深度掌握TS类 - 图1

3.认识函数+原型定义

  1. 函数也是一个对象,当真正开始执行函数,执行环境会为函数分配一个函数对象变量和函数对象空间,函数对象变量用函数名表示,存在栈空间中,函数对象空间是在堆中开辟的一个内存空间,这个空间中有一个默认的prototype属性,这个prototype属性就是一个原型对象属性(也叫对象变量)
  2. 函数和构造函数的区别:通过 new 函数() 时,此刻这个函数就是构造函数
  3. 定义:原型prototype是定义函数由JS自动分配给函数的一个可以被所有构造函数实例对象变量共享的对象变量(也叫对象属性)

    4.如何访问原型对象空间上的属性和方法

  4. 构造函数所有实例对象都可以访问原型对象空间上的属性和方法(每一个实例都有默认的__proto__属性,这个__proto__属性指向原型对象空间)

  5. 关于__proto__:new在创建新对象的时候,会赋予对象一个属性指向构造函数的prototype属性,这个属性就是__proto__
  6. 可以直接通过构造函数的prototype对象属性来访问原型对象空间上的属性和方法。

第一章 全栈思维深度掌握TS类 - 图2

5.实例对象和原型对象空间的关系

1.构造函数实例(也叫对象)如何访问原型对象空间上的属性和方法

  • 构造函数实例访问一个属性和方法,首先从实例空间中查找,如果找到该属性和方法,就停止查找,表示找到了;如果没有找到,就继续在该实例的原型空间中去查找该属性和方法。
  • 实例正式借助自身的__proto__对象属性来查找原型空间中的属性和方法,有点像儿子去向爸爸要他没有的东西一样。

    实例空间:当执行环境执行new 构造函数()时,构造函数中通过this定义的属性和方法会分配在这个空间中。 实例中默认的__proto__对象属性执行原型对象空间。

2.增加和修改原型对象的属性和方法后,所有的实例对象立即可以访问的到【但创建实例后再覆盖原型除外】

  1. function QQUser(QQNo_, QQAge_, QQMark_) {
  2. this.QQNo = QQNo_;
  3. this.QQAge = QQAge_;
  4. this.QQMark = QQMark_;
  5. }
  6. QQUser.prototype.commonfriends = ['xiaoming', 'ale', 'xiaocao'];
  7. QQUser.prototype.show = function () {
  8. console.log(`QQ号:${this.QQNo}, QQ年龄:${this.QQAge}, QQ标注: ${this.QQMark}`);
  9. console.log(`共同的好友:${this.commonfriends}`);
  10. };
  11. console.log('QQUser.prototype:', QQUser.prototype);
  12. let QQZhangsan = new QQUser('2132321', 15, '晴天');
  13. QQUser.prototype.commonfriends.push('大树');
  14. console.log('QQUser.prototype:', QQUser.prototype);
  15. > QQUser.prototype:{commonfriends: Array(4), show: ƒ, constructor: ƒ}
  16. commonfriends: (4) ['xiaoming', 'ale', 'xiaocao', '大树']
  17. show: ƒ ()
  18. constructor: ƒ QQUser(QQNo_, QQAge_, QQMark_)
  19. [[Prototype]]: Object

3.高频面试题:创建实例后再覆盖原型,实例对象无法访问到,为什么?

QQUser.prototype = { commonfriends: ['aaa', 'bbb', 'ccc'] };
这段代码执行的时候,发生了什么?
改变了QQUser构造函数的原型对象空间的指向,指向新的原型对象空间。
但是实例对象的__proto__还是指向原来的原型对象空间。

  1. let QQZhangSan = new QQUser('2132321', 15, '晴天');
  2. console.log('QQUser.prototype:', QQUser.prototype);
  3. QQUser.prototype = { commonfriends: ['aaa', 'bbb', 'ccc'] };
  4. console.log('QQUser.prototype: ', QQUser.prototype);
  5. console.log('QQZhangSan.__proto__', QQZhangSan.__proto__);

第一章 全栈思维深度掌握TS类 - 图3
【思考题】QQZhangSan.__proto__.show()QQZhangSan.show()输出的结果完全一样吗?为什么呢?

3.全站的思维深入理解TypeScript类

1. 学习TypeScript类的深远意义

  • 相对以前JavaScript不得不用构造函数来充当“类”,TypeScript类的出现可以说是一次技术革命。让开发出来的项目尤其是中大项目的可读性好,可拓展性好了不止一点半点。

    可拓展性:对修改关闭,对扩展开放。

  • TypeScript类的出现完全改变了前端领域项目代码编写模式,配合TypeScript静态语言,编译期间就能检查语法错误。项目上线后隐藏语法错误的风险几乎为零,相比于不用TypeSCript开发项目,使用TypeScript后对前端项目尤其是中大项目的开发或底层第三方插件、组件库的开发带来的组成已经超乎了想象。

  • TypeScript类让前端开发人员开发和组织项目或阅读各大前端框架源码的思维方式变得先进了和深化了许多。因为类是OOP的技术基石。
  • 在前端各大流行框架开发的项目中,比如Vue3、Angular、基于Antd UI库的项目、还有后端NodeJS框架,比如Nestjs,亦或是Vue3底层源码,都可以频频见到类的身影。
  • 尽管TypeScript类照搬了Java后端语言的思想,但TypeScript类的底层依然是基于JavaScript的,这一点对于前端工程师深入理解TypeScript打开了一条理解之路,提升他们更深厚的JavaScript功底从而为面试加分都有很大帮助。

    2.TypeScript哪些技能基于类

    TypeScript类是OOP的技术基石,包括类、属性封装、继承、多态、抽象、泛型。紧密关联的技术包括方法重写、方法重载、构造器、构造器重载、类型守卫、自定义守卫、静态方法和属性,关联引用属性,多种设计模式等。

    3.什么是类?

    【定义】类就是拥有相同属性和方法的一系列对象的集合,类是一个模具,是从该类包含的所具体对象中抽象出来的一个概念,类定义了它所包含的全体对象的静态特征和动态特征。
    类有静态特征和动态特征:静态特征称为属性, 动态特征称为方法。

    【订单类】 静态特征【属性】:订单号,下单时间,下单顾客,订单详情,顾客微信,收件地址,负责客服 动态特征【方法】:下单,修改订单,增加订单,删除订单,查询订单,退单(这一些方法真正开发会归为OrderService类,但从广义来说都属于订单系列类的方法) 类上定义的属性一定是描绘这个类本身特征的变量,不能把一些无关的变量定义成类属性

4.理解子类

(1)什么是子类?
有两个类,比如 A 类和 B 类,如果满足 A 类 is a kind of B类,那么 A 类就是 B 类的子类 比如:A 类是顾客类,B 类是人类,因为顾客类 a kind of 人类成立【顾客类是人类的一种】,所以顾客类是人类的子类。
(2) 子类如何继承父类的属性和方法?
以顾客类为例子:顾客类继承了父类【人类】的非私有的属性和方法,也具备子类独有的属性和方法 。
顾客类继承父类【人类】的全部非私有的属性和方法外,还有哪些独有的属性和方法呢? 顾客类独有属性:顾客等级,顾客编号 顾客类独有方法:购买

5.什么是对象?

对象也叫做实例,对象变量也叫做实例变量
(1) 什么是对象(实例)?
就是一个拥有具体属性值和方法的实体,是类的一个具体表现,一个类可以创建一个或者多个对象。
(2) 如何通过类来创建对象(实例)?

  1. let 对象变量名 = new 类名() const 对象变量名 = new 类名()

(3) 如何根据People类来创D建叫张三对象(实例)的人?【举例】
let kateCust = new Customer()
kateCust是对象变量名 ,new Customer() 表示 new 出来的是一个Customer对象,而且是运行期间才在堆中分配Customer对象的内存空间(new 就是分配内存空间的意思)
(4)类的对象变量、对象内存图展示
第一章 全栈思维深度掌握TS类 - 图4
(5)类的对象变量和对象的关系
类的对象变量存在栈中,对象变量存储着对象的首地址,对象变量通过这个地址找到它的对象

  1. class Person {
  2. // public name: string | undefined; // typescript4.0之前属性如果没有赋值的解决方法,增加一个undefined数值类型
  3. // 类上定义的属性一定是描绘这个类本身特征的变量,不能把一些无关的变量定义成类属性
  4. public name: string = 'noname';
  5. public age: number = 0;
  6. public phone: string = '1111';
  7. // 对象的变量=实例的变量=类的非静态属性=简称属性
  8. // 【实例属性】或【对象属性】
  9. constructor(name_: string, age_: number, phone_: string) {
  10. this.name = name_;
  11. this.age = age_;
  12. this.phone = phone_;
  13. }
  14. public eat(who: String, address: string): void {
  15. console.log(`${this.name}和${who}一起吃饭,在${address}吃饭`);
  16. }
  17. }
  18. // 创建对象一共做了三件事
  19. // 1.在堆中为类的某个对象(实例)分配一个内存空间
  20. // 2.调用对应的构造函数(构造器):new Person()自动匹配无参数的构造器
  21. // 3.第三件事情,把对象赋值给对象变量
  22. let zhangsan = new Person('张三', 23, '12312323423');
  23. zhangsan.eat('李四', '中关村');
  24. console.log(zhangsan);

6.TypeScript类编译后的JS代码(ES5语法)

  1. var Person = /** @class */ (function () {
  2. function Person(name_, age_, phone_) {
  3. this.name = 'noname';
  4. this.age = 0;
  5. this.phone = '1111';
  6. this.name = name_;
  7. this.age = age_;
  8. this.phone = phone_;
  9. }
  10. Person.prototype.eat = function (who, address) {
  11. console.log(
  12. ''.concat(this.name, '\u548C').concat(who, '\u4E00\u8D77\u5403\u996D\uFF0C\u5728').concat(address, '\u5403\u996D')
  13. );
  14. };
  15. return Person;
  16. })();
  17. var zhangsan = new Person('张三', 23, '12312323423');
  18. zhangsan.eat('李四', '中关村');
  19. console.log(zhangsan);

立即执行函数的作用:立即执行函数避免了变量名被污染
[译] JavaScript:立即执行函数表达式(IIFE)

7.深度掌握 TypeScript 引用属性

1. 如何理解类的引用属性(引用类型属性)

什么是引用属性?:如果类中的属性的类型是引用属性,那么这个属性就是引用属性
引用属性的数据类型一般有数组、函数、类、对象、对象数组、集合类(Set、Map、自定义集合类)
引用属性的经典应用场景

  1. 底层经典案例:如果我们使用TypeScript来开发一个ES6的Set集合类就是对数组的二次包装,在这个Set集合类中就需要包含一个数组的引用属性提供Set类的各个方法来使用。
  2. 底层经典案例:Promise是前端很重要的技术,Promise底层类中就采用了函数类型的引用属性(知晓即可,后面的章节会自己手动开发一个Promise)。
  3. 二次封装应用场景:Set集合虽好,但是不能使用 get(index) 直接取值,这也造成了取值不方便,如果我们自己手动封装一个包含了add、get、remove、delete、query的集合类 ArrayList ,这时也需要借助数组的引用属性。
  4. 各种Nodejs后端项目构建的应用场景:以订单详情类和订单类为例
  5. 跨前端领域的Java后端大量使用了引用属性。

    2. 类的引用属性真实应用场景:订单详情类和订单类

    一个订单对象有多个订单详情对象
    【订单类产生过程】
    每一个顾客每下一次单,就会生成一个或者多个订单详情(一件商品生成一个订单详情),但每次只能生成一个订单,也就是一个订单中包含了一个或者多个订单详情,我们可以定义一个 订单类 Order
    订单类包含了订单id,订单日期,顾客地址,顾客名,顾客微信,顾客手机号,客服
    【订单详情类产生过程】
    顾客在淘宝上下一次订单购买了三件商品,用三条记录来表示:
    订单详情1: 1 “笔记本” 6898 8
    订单详情2: 2 “笔记本” 7878 9
    订单详情3: 3 “手机” 3878 2
    每一个订单详情都可以用一个对象来表示 订单详情对象
    创建一个订单详情类 OrderDetail,然后new出来3个订单详情类的实例。

OrderDetail.ts

  1. export default class OrderDetail {
  2. public orderDetailId: number = 0;
  3. public productName: string = "noproduct";
  4. public price: number = 0;
  5. public count: number = 0;
  6. constructor(orderDetailId_: number, productName_: string, price_: number, count_: number) {
  7. this.orderDetailId = orderDetailId_;
  8. this.productName = productName_;
  9. this.price = price_;
  10. this.count = count_;
  11. }
  12. }

Order.ts

  1. import OrderDetail from "./OrderDetail";
  2. class Order {
  3. // 订单id,订单日期,顾客地址,顾客名,顾客微信,顾客手机号,客服
  4. public orderId: number = 0;
  5. public date: Date = new Date();
  6. public custname: string = "nocustname";
  7. public phone: string = "1111111";
  8. public orderDetails: Array<OrderDetail> = [];
  9. constructor(
  10. orderId_: number,
  11. date_: Date,
  12. custname_: string,
  13. phone_: string,
  14. orderDetails_: Array<OrderDetail>
  15. ) {
  16. this.orderId = orderId_;
  17. this.date = date_;
  18. this.custname = custname_;
  19. this.phone = phone_;
  20. this.orderDetails = orderDetails_;
  21. }
  22. }
  23. let orderDetail1 = new OrderDetail(10, "电视机", 5000, 3);
  24. let orderDetail2 = new OrderDetail(11, "桌子", 300, 3);
  25. let orderDetailArray: Array<OrderDetail> = [orderDetail1, orderDetail2];
  26. let orderDate = new Date(2022, 1, 1, 5, 20, 0);
  27. let order = new Order(1, orderDate, "李四", "333333", orderDetailArray);
  28. console.log(order);

8. 构造器简洁的属性赋值和TypeScript4中的新特性

构造器简洁赋值

给构造器的参数如果加上 public ,这个参数就变成了一个属性
这种简洁写法是两步综合体:
第一步:定义了一个属性
第二步:等于默认构造函数会给这个属性赋值

  1. class Order {
  2. constructor(
  3. public orderId: number,
  4. public date: Date,
  5. public custname: string,
  6. public phone: string,
  7. public orderDetailArray: Array<OrderDetail>
  8. ) {}
  9. }

TypeScript4的新特性

  1. class OrderDetail {
  2. // TS4之前针对没有初始化的值,
  3. // 也没有在构造函数中给这个变量赋值的一种解决方案: 使用undefined
  4. public orderDetailId: number | undefined;
  5. public productName: string | undefined;
  6. // 如果不加感叹号,后面使用this.price会报错
  7. // TS之后感叹号表示忽略
  8. public price!: number;
  9. // public price: number | undefined;
  10. public count: number;
  11. constructor(orderDetailId_: number, productName_: string, count_: number) {
  12. this.orderDetailId = orderDetailId_;
  13. this.productName = productName_;
  14. // this.price = price_;
  15. this.count = count_;
  16. }
  17. public getTotal(): number {
  18. return this.price * this.count;
  19. }
  20. }
  21. let orderDetail = new OrderDetail(12, "phone", 30);
  22. console.log(orderDetail.price); // undefined
  23. console.log(orderDetail.getTotal()); // NaN

9. TS类和ES6类的区别

TS 类和 ES6 类看着很像,但又有很多不同,区分 TS 类和 ES6 类,既可以让我们对 TS 类的优势印象更深刻,也会减少 TS 类和 ES6 类概念上的混淆。

1. 定义类属性的方式不同

TS有多种定义属性的方式,如下:

  1. 现在类中定义属性然后在构造函数中通过this赋值
  2. 构造函数直接为参数添加修饰符public,这个参数就变成了一个属性,默认构造函数会给这个属性赋值(隐式操作)

ES6依然沿袭了JavaScript的赋值方式,在构造函数中直接this来定义属性并赋值,代码如下:

  1. class Order {
  2. constructor(orderId, date, custname, phone, orderDetailArray) {
  3. this.orderId = orderId;
  4. this.date = date;
  5. this.custname = custname;
  6. this.phone = phone;
  7. this.orderDetailArray = orderDetailArray;
  8. }
  9. }

2. ES6类没有访问修饰符,TS类自带访问修饰符

ES6 类暂时还没有访问修饰符 public protected private,这也让ES6 类设置访问权限变的异常麻烦,即使借助 call 方法或者 symbol 类型设置了访问权限局限性也很大,其实也并没有真正彻底解决访问权限的问题。
TS 类却自带 public protected private 三种访问权限,设置访问权限轻松自如。

3. TS 类是静态类型语言的类,而 ES6 类按照 JavaScript 的一个语法标准设计而成

TS 是静态类型语言,具有类型注解和类型推导的能力,项目上线后隐藏语法和类型错误的的风险几乎为零,而 ES6 是 JavaScript 的一个语法标准,没有数据类型检查的能力,举一个简单例子来说明问题。

  1. // ES6
  2. const x = 3;
  3. x = 10; //ES6没有任何语法错误提示
  4. // TS
  5. const x = 3;
  6. x = 10; //无法分配到 "x" ,因为它是常数。

4. TS 类可以生成 ES5 或 ES6 或以上版本的 js 文件

通过设置 tsconfig.json 文件的 target 属性值为 ES6,那么生成的 js 文件就是 ES6 版本的 js 文件。