1. 重载
1. 函数重载,方法重载的重要性和优势
重要性
著名前端流行框架底层都用到函数重载,例如:Vue3 底层源码就多处使用到带泛型的函数重载。很多前端面试更是拿函数重载作为考核求职者TS技能是否扎实的标准之一,如果你不掌握函数重载,等于你的 TS 技能有缺失,技能不过关。
函数重载或方法重载适用于完成项目种某种相同功能但细节又不同的应用场景: 我们举一个生活中的例子让同学们先有个印象,比如:吃饭是一个函数,表示一个吃饭功能,但西方人用叉子,中国人用筷子,这就是细节不同,那如果我们可以用函数重载来解决。
不管现阶段你公司的项目中是否用到了函数重载和方法重载,如果学完后,你能适时给公司提建议,建议项目中合适的场景中使用函数重载并说明原因,你的建议应该很受欢迎。
优势
- 结构分明:让代码可读性,可维护性提升许多,而且代码更漂亮。
- 各司其职,自动提示方法和属性:每个重载签名函数完成各自的功能,输出取值时不用强制转换就能出现自动提示,从而提升开发效率。
- 更利于功能拓展
2. 实现微信消息发送的函数
真实应用需求:有一个获取微信消息发送接口查找函数,根据传入的参数从数组中查找数据,如果入参为数字,就认为消息id,然后从后端数据源中找到对应id的数据并返回,否则返回这一类型的全部消息。
type MessageType = "image" | "audio" | string;type Message = {id: number;type: MessageType;sendmessage: string;};let messages: Array<Message> = [{id: 1,type: "image",sendmessage: "你好啊,今晚咱们一起去三里屯吧",},{id: 2,type: "audio",sendmessage: "朝辞白帝彩云间,千里江陵一日还",},{id: 3,type: "audio",sendmessage: "你好!张无忌",},{id: 4,type: "image",sendmessage: "刘老根苦练舞台绝技!",},{id: 5,type: "image",sendmessage: "今晚王牌对王牌节目咋样?",},];
如果不使用函数重载,主要存在两个问题:
- 由于无法知道函数返回的数据类型,不能自动推导,只能强制转换。
- 函数的写法比较长,类型不明确。
```typescript
function getMessage(value: number | MessageType): Message | undefined | Array
{ if (typeof value === “number”) { return messages.find((msg) => msg.id === value); } else { return messages.filter((msg) => msg.type === value); } }
console.log(getMessage(“audio”)); console.log(getMessage(2));
<a name="DC4KJ"></a>## 3. 函数重载的定义TS的函数重载比较特殊, 和很多其他后端语言的方法重载比起来,多了不少规则。学习函数重载,先要了解什么是函数签名,定义如下:<br />_ 函数签名 function signature _<br />_函数签名 = 函数名称 + 函数参数 + 函数参数类型 + 返回值类型_ 四者合成。在TS函数重载中,包含了_实现签名_和_重载签名_,实现签名是一种函数签名,重载签名也是一种函数签名---_ 不完整模糊的TS函数重载定义 _<br />一组具有相同名字,不同参数列表的和返回值无关的函数。_ 完整的TS函数重载定义 _<br />包含了以下规则的的一组函数就是函数重载。1. 由 _一个实现签名 + 一个或多个重载签名_ 合成。1. 但外部调用重载函数定义的函数时,_只能调用重载签名,不能调用实现签名_。这看似矛盾的规则,其实是TS的规定:实现签名下的函数体是给重载签名编写的,实现签名只是在定义时起到了统领所有重载签名的作用,在执行调用时就看不到实现签名了。1. 调用重载函数时,会_根据传递的参数来判断_你调用的是哪一个函数。1. 一个函数体,_只有实现签名配备了函数体_,所有的重载签名都只有签名,没有配备函数体。1. 参数类型:实现签名参数个数可以少于重载签名的参数个数,但是实现签名如果准备包含重载签名某个位置的参数,那实现签名就必须兼容所有重载签名该位置的参数类型。1. 返回值类型:1. 必须给重载函数提供返回值类型,TS无法默认推导。1. 提供给重载签名的返回值类型不一定为其执行时的真实返回值类型,可以为重载签名提供真实返回值类型,也可以提供void或者unknown或any类型,如果重载签名的返回值是void、unknown或any类型,那么将由实现签名来决定重载签名执行时的真实返回类型。当然,为了调用时能有自动提示、可读性、避免出现类型强制转换,_强烈建议为重载函数提供真实的返回值类型_。1. 不管重载签名返回值类型是何种类型,实现签名都可以返回any或者unknown类型,当然我们一般两者都不选择,让TS默认为实现签名自动推导返回值类型。```typescript//重载签名函数可以有多个function getMessage(id: number): Message;function getMessage(msgType: MessageType): Message[];function getMessage(msgType: MessageType, recordCount: number): Message[];//实现签名函数,只有实现签名才有函数体,实现签名只能有一个打字function getMessage(payload_frompage: any,recordCount: number = -1): Message[] | Message | undefined {if (typeof payload_frompage === "number") {return messages.find((msg) => msg.id === payload_frompage);} else if (recordCount === -1) {return messages.filter((msg) => msg.type === payload_frompage);} else {return messages.filter((msg) => msg.type === payload_frompage).slice(0, recordCount);}}let msgs = getMessage("image", 2);let msgsAll = getMessage("image");let msg = getMessage(1);console.log("msgsAll ----> ", msgsAll);console.log("msgs ----> ", msgs);console.log("msg ----> ", msg);
4. 方法重载
方法和函数的区别
方法 :方法是一种在特定场景下的函数,有对象变量直接调用的函数都是方法。比如:
- 函数内部用this定义的函数是方法
- TS类中定义的函数是方法
- 接口内部定义的函数是方法
- type内部定义的函数是方法
方法签名 : 和函数签名一样,方法签名 = 方法名称 + 方法参数 + 方法参数类型 + 返回值类型四者合成
实现ArrayList类
// 1.对现有的数组进行封装,让数组增删改变得更加好用// 2.提供get方法 remove方法 显示方法【add方法】// 其中需求中的remove方法有两个,我们用方法重载来实现export default class ArrayList {constructor(public elements: Array<object>) {}get(index: number) {return this.elements[index];}show() {this.elements.forEach((ele) => {console.log(ele);});}remove(value: number): number;remove(value: object): object;remove(value: any): number | object | undefined {this.elements = this.elements.filter((ele, index) => {if (typeof value === "number") {return index != value;} else {return ele !== value;}});return value;}}let stuOne = { stuname: "wnagwu", age: 23 };let stuTwo = { stuname: "lisi", age: 39 };let stuThree = { stuname: "liuqi", age: 31 };let arrayList = new ArrayList([stuOne, stuTwo, stuThree]);arrayList.show();console.log("删除第一个学生");let value = arrayList.remove(0);console.log("删除的元素为第:", value, "学生");arrayList.show();console.log("删除第二个学生");let value2 = arrayList.remove(stuTwo);console.log("删除的学生对象为:", value2);arrayList.show();// 如果是根据数字【元素索引】去删除元素,remove方法返回的是一个数字// 如果是根据对象去删除元素,remove方法返回的是一个对象
5. 构造器重载
1. 再次强化理解this
this其实是一个对象变量,当new出来一个对象时,构造器会隐式返回this给new对象等号左边的对象变量,this和等号左边的对象变量都指向当前正在创建的对象。
对象变量存储在栈空间中,指向对象。
以后,哪一个对象调用TS类的方法,那么这个方法中的this都指向当前正在使用的对象。this和当前的对象变量中都保存着当前对象的首地址。
class MyClass {constructor(){...// 隐式返回this,等同于:// return this}}
2. 创建对象的过程
创建对象一共做了三件事:
- 在堆中为类的某个对象(实例)分配一个空间
- 调用对应的构造函数并且把构造器中的各个参数值赋值给对象属性
- 把对象赋值给对象变量
3. TS构造器有返回值吗?
尽管TS类构造器会隐式返回this,如果我们非要返回一个值,TS类构造器只允许返回this,但构造器不需要返回值也能通过编译,更没有返回值类型之说。从这个意义上,TS构造器可以说是没有返回值这一说的构造函数。
_【注意】_TS构造器和JS构造器函数关于返回值的说法不完全相同。4. 构造器重载的意义
构造器重载和函数重载基本相同,主要区别是:TS类构造器重载签名和实现签名都不需要管理返回值,TS构造器是在创建对象出来之后,但是还没有给对象赋值之前被执行,一般用来给对象属性赋值。
我们知道在TS类中只能定义一个构造器,但实际应用时,TS类在创建对象京城许村用到有多个构造器的场景,比如:我们计算一个正方形面积,创建正方形对象,可以给构造器传递宽和高,也可以给构造器传递一个包含了宽和高的形状参数对象,这样需要构造器函数重载来解决。而面试中也多次出现TS构造器函数重载的考察,主要考察求职者对重载+构造器的综合应用能力。5. 构造器是方法吗?
我们说对象调用的才是方法,但是Ts构造器是在对象空间地址复制给对象变量之前被调用,而不是被对象变量调用的,所以构造器可以说成构造函数,但不能被看成是一个方法。6. 构造器实现编码
```typescript // 计算长方形面积 type chartParams = { width?: number; height?: number; radius?: number; };
class Rect { public width: number; public height: number;
constructor(width: number, height: number); constructor(paramsObj: chartParams); constructor(paramsObjOrWidth: any, height: number = 0) { if (typeof paramsObjOrWidth === “object”) { this.width = paramsObjOrWidth.width; this.height = paramsObjOrWidth.height; } else { this.width = paramsObjOrWidth; this.height = height; } }
public getArea(): number { return this.height * this.width; } }
let rect = new Rect(40, 20); let rect2 = new Rect({ width: 50, height: 50 }); console.log(rect.getArea()); console.log(rect2.getArea());
<a name="mw8KO"></a># 2. 单件设计模式&静态方法和静态属性<a name="ay234"></a>## 1. 设计模式概述<a name="QdU02"></a>### 1. 了解设计模式设计模式通俗地讲,就是一种_更好的编写代码方案_,打个比如:从武汉到上海,可以坐飞机、轮船、汽车、骑摩托多种方式,把出行看成变成,那么选择飞机相对就是一个更好的解决方案。<a name="g6HE0"></a>### 2. 常见的设计模式常见的设计模式有单件设计模式,简单工厂设计模式,工厂方法,抽象工厂设计模式,观察者模式,装饰设计模式,代理设计模式,MVC,MVP,MVVM架构设计模式。本课程讲解单件设计模式,原因有两个:1. 设计模式并非TypeScript课程的重点。2. 单件模式短小精悍,能更好地帮助掌握TS类,类的静态方法,类构造器,类对象的联合运用。<a name="WpkIy"></a>### 3.单件设计模式的两种定义** 简明定义一 **:一个类对外有且仅有一个实例,也就是只提供一个实例,这种编码方案就是单件设计模式。<br />** 完整定义一 **:如果某个类始终对外只提供一个对象,并且在该类的内部提供了一个外部访问该对象的方法或者对象属性,那么这种编写代码方案就是单件设计模式。<br />** 完整定义二 **:如果一个类的任何外部通过访问类提供的某个方法或者某个属性始终只能获取该类的一个对象,但该类提供了多个外部可以访问的方法或者属性,那么外部就能访问到该类的多个不同对象。但从实际开发看,绝大多数情况的应用场景,我们对象都只提供一个唯一的可以访问的方法或者属性,这样就保证了单例为单个。<a name="OnCEM"></a>### 4. 何时需要时用单件设计模式?实际开发中,外部访问某个类的对象时,确保只能访问该类的唯一对象时才能保证逻辑正确性时,这时就应该使用单件设计模式了。<a name="E1cdg"></a>### 5. 前端领域单件设计模式的真实应用场景**应用场景1:**比如 Vuex,React-Redux 中的全局状态管理容器 store 对象在整个项目被设计成唯一的对象,把 store 对象所在 的类设计成单件设计模式将是最好的设计方案 。(当然也可以有其他替代写法)<br />**应用场景2:**一般前端项目需要进行客户端本地数据存储时,都会考虑使用 localStorage,localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份 localStorage 数据。那封装 localStorage设计成一个单件设计模式类就再合适过了。【尽管也可以其他写法,但依然存在问题,编码时我们会给同学们说明】。<br />**应用场景3:**我们知道项目日志记录是一个项目中必不可少的环节,当我们为一个项目编写一个日志文件类,用来保存日志和阅读日志信息时,这个日志文件类可以有多种设计方案,但把类写成单件模式是最好的方案,因为每次存储日志信息到日志文件上时都创建一个日志对象,这既没有必要,也很浪费内存空间。<a name="xln9A"></a>## 2.构建单件MyLocalStorage类不使用类,有以下问题<br />问题一:代码零散<br />问题二:可读性差<br />问题三:对后期维护产生影响<br />问题四:JSON相关的代码可以放到类中,如果这样写的多,就影响了开发效率```typescriptlocalStorage.setItem("count", "30");let loginInfoObj = { username: "xiaoming", age: 23 };localStorage.setItem("loginUser", JSON.stringify(loginInfoObj));let value = localStorage.getItem("loginUser");value ? console.log(JSON.parse(value)) : null;
构建单件设计模式(懒汉式,即延迟使用式)的步骤
- 第一步:把构造器设置为私有的,不允许外部来创建类的实例
- 第二步:至少提供一个外部访问的方法或属性,外部可以通过这个方法或属性来得到一个实例,所以应该把这个方法设置为静态方法。
第三步:外部调用第二步提供的静态方法来获取一个对象
静态方法
- 带static关键字就是一个静态方法
- 静态方法和对象无关, 外部的对象变量不能调用静态方法和静态属性
- 外部可以通过类名来调用
静态方法不可以访问实例属性或者实例方法
export default class MyLocalStorage {// 静态属性和实例属性是类中的两大属性static localStorage: MyLocalStorage;private constructor() {}//提供一个外部访问的方法,//通过这个方法来提供得到一个对象的方法public static getInstance() {if (!this.localStorage) {this.localStorage = new MyLocalStorage();}return this.localStorage;}public setItem(key: string, value: any) {localStorage.setItem(key, JSON.stringify(value));}public getItem(key: string) {let value = localStorage.getItem(key);return value ? JSON.parse(value) : null;}}
3.结合单件设计模式深入静态方法、静态属性
1. 静态成员几个规则
外部如何调用TS类的静态成员?
类名直接调用静态成员,格式:类名.静态属性类名.静态方法- TS类的一个静态方法如何调用其他的静态成员?
使用this来获取静态成员。
- 静态方法可否访问类中原型对象上的方法或者对象属性,反过来呢?
都不可以
- 对象变量能否访问静态成员?
不能
一个静态方法改变了某个静态属性,其他静态方法或者类外部任何地方访问这个属性都会发生改变。
2. 静态成员保存在内存哪里?何时分配内存空间?
答:任何一个TS类中的静态成员存储在内存的静态区,运行一个TS类,TS会首先为静态成员开辟内存空间,静态成员的内存空间分配的时间要早于对象空间的分配,也就是任何一个对象创建之前TS就已经为静态成员分配好了空间。但一个静态方法只会分配一个空间,只要当前服务器不重启或者控制台程序还没有结束之前,那么静态方法一直存在内存空间,无论调用多少次这个静态方法,都是调用的同一空间。
静态方法总结无论你是否创建对象,创建多少对象,是否调用该静态方法或者静态属性,TS都会为这个静态方法或者静态属性分配内存空间,注意:静态成员和对象无关。
- 一旦为静态方法或者静态属性分配好空间,就一直保存在内存中,直到服务器重启或者控制台程序执行结束才会被释放。
【注意】 new一个TS类的方法可以吗?能在TS类外部使用prototype为TS类增加方法或者属性吗?
虽然在JS中可以new一个类内部定义的对象方法或者静态属性,但TS已经屏蔽了去new一个类中的方法(JS可以,会成为一个构造函数)。TS类可以访问prototype原型对象属性,也可以覆盖类上已经存在的方法,但无法在prototype原型对象属性增加新的方法或属性。这么做,就是出让我们只能在类的内部定义方法,防止回到ES5之前非面向对象的写法。

3. 静态方法或属性和原型对象空间上的方法有什么区别?
原型对象空间上的方法和属性是用来提供给该类的所有对象变量共用的方法或属性,没有对象和对象变量,原型上的属性和方法就没有的用武之地。而静态方法或者静态属性属于类,可以通过类来直接访问。任何一个对象创建之前TS就已经为静态成员分配好了空间。但一个静态方法或者静态属性只会分配一个空间,而每一个对象都有自己独立的空间。
4. 静态方法是否可以接受一个对象变量作为方法的参数?
可以。静态方法不能通过this来访问对象属性和方法,但可以通过调用静态方法时,把对象变量传递给静态方法来使用。比如:我们把js的Object构造函数想象成一个TS类,Object类就拥有大量的静态方法,例如:apply,call,bind,keys等。
// 我们现在把 Object 构造函数看成一个 Object 类,创建 Object 类的对象。let obj = new Object({ username: "wangwu", age: 23 })//1let obj2 = { username: "wangwu", age: 23 }// 2是1的简写// 把 obj 对象变量传递给 keys静态方法,obj对象变量作为 keys 静态方法的参数Object.keys(obj2)
5. 何时应该把一个方法定义成静态方法?或者把一个属性定义成静态属性呢?
应用1 :单件设计模式就是静态方法和静态属性很好的应用场景之一。当外部不能创建对象,就只能借助类内部的静态方法来获取类的对象;这是肯定不能把这个方法定义成原型对象属性上的方法,只能定义成类的静态方法。因为如果定义成原型对象属性的方法,就会导致外部无法访问,原因是外部根本不能创建对象,也就无法访问原型对象属性上的方法。而静态方法要访问的属性就只能是静态属性了,这也是静态属性的应用时机。
应用2 :当类中某个方法没有任何必要使用任何对象属性时,而且使用了对象属性反而让这个方法的逻辑不正确,那既如此,就应该禁止这个方法访问任何对象属性和其他的对象方法,这时就应该把这个方法定义为静态方法。例如:一个顾客类的购买方法buy()中肯定要允许访问顾客姓名或其他顾客微信这些对象属性,这样的方法我们就需要定义在原型对象属性上,但如果顾客类中的阅读顾客积分公告方法readNotice() 是针对全体顾客的公告方法,就应该定义为静态方法,方法内部就应该禁止出现任何具体的对象属性。如果在这样的方法中使用了顾客的某个属性,比如用了顾客姓名,那么这个方法逻辑就不正确。(这个方法就会说:你让我向全体顾客展示公告,你我要知道每个顾客姓名做什么?)所以我们应该让这样的方法禁止访问对象属性和其他的对象方法,那就应该设置为静态方法。
应用3 :当一个类中某个方法只有一个或者1-2个对象属性,而且更重要的是,你创建这个类的对象毫无意义,我们只需要使用这个类的一个或者多方法就可以了,那么这个方法就应该定义为静态方法。常见的工具类中的方法通常都应该定义为静态方法。比如 StringUtil, FileUtil 等,我们以 FileUtil 为例进行讲解
思考题 :定义一个文件工具类 FileUtil,编写一个读取文件方法 readFile()方便外部调用,那这样的方法应该定义为静态方法吗?
答:定义在原型属性上和定义为静态方法似乎都可以,只要 readFile() 方法获取到外部提供文件名就可以展开文件读写。请看下面两段代码,我们仔细比较后再来决定用哪一种方案?
class FileUtil{// 从指定文件上把数据读出来打印在控制台或页面上的静态方法public static readFile(readonly fileName:string){fs.readFile(fileName, (err: any, data: any) => {console.log("fs.readFile:", data.toString());}}// 把键盘输入的数据或页面上获取的数据写入到指定文件上的静态方法public static writeFile(fileName:string){fs.writeFile(fileName, '刘老根4', function (error) {if (error) {console.log('写文件失败')} else {console.log('写文件成功')}})}// 实际应用中,读和写一般都不在一个时间段,可能读功能完成后,过了几分钟,用户才在客户端执行写的方法,// 又过了一会,用户又在客户端执行了读的方法。 但我们知道静态方法实际上是一直保存到内存空间,这样反复操作其实节省了大量反复创建 和释放 FileUtil 对象的时间和对应的对象内存空间。FileUtil.readFile('./log.txt')FileUtil.writeFile('./log5.txt')
class FileUtil{constructor(public fileName:string){}// 从指定文件上把数据读出来打印在控制台或页面上的静态方法public readFile(){fs.readFile(fileName, (err: any, data: any) => {console.log("fs.readFile:", data.toString());}}// 把键盘输入的数据或页面上获取的数据写入到指定文件上的静态方法public writeFile(fileName:string){fs.writeFile(fileName, '刘老根4', function (error) {if (error) {console.log('写文件失败')} else {console.log('写文件成功')}})}// 实际应用中,读和写一般都不在一个时间段,可能读功能完成后,过了几分钟,用户才在客户端执行写的方法,// 又过了一会,用户又在客户端执行了读的方法。所以每次都要创建 FileUtil 对象,// 这样反复创建和释放FileUtil 对象,就浪费了大量的时间和对应的对象内存空间new FileUtil('./log.txt').readFile()new FileUtil('./log5.txt').writeFile()
【注意】对于本条关于静态属性或者方法的解决方案绝对不能用在学生、顾客等其他场景,那样就会导致三个比较严重的问题,以学生为例:
- 浪费了很多不必要的内存空间。运行一开始就为大量的静态属性和大量的静态方法分配空间。很可能某个静态方法一直没有使用,白白占用着内存空间。
- 无法展示一个学生一个对象的直观效果,完全失去了对象来描述实体的优势。
- 最严重的问题是:属性值一直在变化。
所有的操作都用一个静态方法空间来完成某种功能,一旦某个操作改变了静态方法中的某个值,比如改变了学生姓名,则其他操作访问到这个静态变量看到的结果就全变了。
6. ⭐饿汉式单件设计模式
饿汉式单件设计模式是无论你是否用到了实例,一开始就创建这个唯一的对象。
构建饿汉式单件设计模式 立即创建对象
- 第一步:把构造器设置为私有的,不允许外部来创建类的的实例
- 第二步:建立一个静态引用属性,同时把这个静态引用属性直接指向一个对象
new MyLocalStorage() - 第三步:外部调用第二步提供的静态方法来获取一个对象
如果确定使用单件设计模式,这种方案更加简便!
实际的项目中:饿汉式单件设计模式使用的更多
export default class MyLocalStorage {// 静态属性和实例属性是类中的两大属性static localStorage: MyLocalStorage = new MyLocalStorage();private constructor() {}public setItem(key: string, value: any) {localStorage.setItem(key, JSON.stringify(value));}public getItem(key: string) {let value = localStorage.getItem(key);return value ? JSON.parse(value) : null;}}MyLocalStorage.localStorage.setItem("count", 3);MyLocalStorage.localStorage.getItem("count");
3. 继承
1. 学习TS继承的意义
练就更深厚的 JS 原型、原型链功底
TS编译后的JS中有经典的JS原型和原型链的源码实现,虽然稍显复杂,但源码并不长,这将是练就更深厚的 JS 原型、原型链功底的绝佳场景。这不仅让你日后面试大受益,而且也为你能阅读Vue3、React源码或其他流行框架源码铺路,因为不管是那种源码,JS原型链继承一定会用到,再加上你的TS功底,那么这些都成让你日后前端之路走的更远,走的更高!
提升前端项目架构的根基技术
如果要你现在用开发一个工具库,组件库,你打算怎么开发 ? 可以写出n多个版本的代码,都可以实现,但版本和版本之间的价值却差别巨大,你可以用 JS 原型写出1年左右工作经验的前端水准的代码。当然,上乘之选肯定是用 TS 来开发,你也可以灵活运用TS继承,多态等多种技术写出高水准的代码。但如果你不具备后端思维能力,就算你工作了5年,你也不一定能有这样的思维,甚至随时有可能被一个拥有了后端思维的只有1到2年工作经验水准的前端工程师超越。
突破前端技术瓶颈之一的技能,晋级中高级前端工程师必会技能
- 如果你只掌握了单个类的使用,而不知道如何运用继承,那这也是技能缺失,将会限制你日后技术发展的高度,限制你的技术视野,让你的前端变得过于前端化。
- 说深度掌握了 TS 继承就能突破所有的前端技术瓶颈,那很显然是夸大其词,但要想突破前端技术瓶颈,深度掌握继承必然是其中一项技能,而且是根基技术之一,可见继承的重要性不言而喻。
- 比如一个简单的汽车租赁项目,让你来实现,你把前端功能实现了,展示在页面上了,但是打开你用 TS 写的 Vuex 代码,用 TS 写的 Nodejs 代码,过于前端化的思维让你编写的代码可能让人不堪入目。这里不单单是说用到封装继承、多态、解耦这些技术,更多的是你过于前端化的思维编写的项目可扩展性将非常差,可读性也差,复用性也低,而这些是评判一个项目是否值钱的关键因素。
- 如果你希望未来职业生涯拥有更广阔的技术视野;更远的未来你甚至希望自己能胜任技术总监,那么你就一定从一个更广阔的技术视野来提升自己的技术能力,不能让自己被框在过于前端化的路上。
虽然老师不能三言两语给同学们描述出什么才叫完全突破前端瓶颈,但有一点是可以肯定的,就是要有一定的后端思维能力,这里当然不是要拥有 Java 后端能力,而是起码具备 Nodejs 后端的项目架构能力,Nodejs 可以前端工程师提升晋级一定要掌握的技能。而深度掌握了 TS 继承已经为突破前端技术瓶颈开了一个好头。
2. 原型链继承
1. 原型链继承实现原理
【说明】Parent类是父构造函数,Son类是子构造函数
原型链继承基本思想就是Son类的原型对象属性_Son.prototype_指向_new Parent()_。
原型连继承实现的本质就是改变Son构造函数的原型对象变量的指向,那么Son.prototype就可以访问Parent对象空间中的属性和方法。顺着proto,Son类也可以访问Parent类的原型对象空间中的所有属性和方法。
function Parent(name, age) {this.name = name;this.age = age;}function Son(favor, sex) {this.favor = favor;this.sex = sex;}Son.prototype = new Parent("xiaoming", 23);Son.prototype.constructor = Son;let son = new Son("basketball", "male");
没有原型链继承的情况
2. 原型链继承带来的好处
好处一 子类对象变量可以访问父类的实例属性
好处二 子类对象变量可以访问父类原型对象空间中的属性和方法
原型链继承的完整描述:子对象首先在自己的对象空间中查找要访问的属性和方法。如果找到,就输出,如果没有找到,就沿着子对象中的proto属性指向的原型对象空间中查找有没有这个属性或者方法。如果找到,就输出,如果没有找到,继续沿着原型对象空间中的proto查找上一级原型对象空间中的属性或者方法,直到找到Object.prototype原型对象指向的原型对象空间为止,如果再找不到,则输出null。
3. ⭐原型链继承最容易被遗漏的一步
Son.prototype.constructor = Son
4. 原型链继承的不足
局限性:不能通过子类构造函数向父类构造函数传递参数。
3. 借用构造函数继承
1.借用构造函数解决原型链继承的局限性
借用构造函数继承的思想就是在子类的内部,借助apply()和call()方法调用并传递参数给父类,在父类构造函数中为当前的子类对象变量增加属性。
apply()和call()方法的作用:以后 Function 章节详细分析。
function Parent (name, age) {this.name = namethis.age = ageconsole.log("this:", this)console.log("this.name:", this.name)}Parent.prototype.friends = ["xiaozhang", "xiaoli"]Parent.prototype.eat = function () {console.log(this.name + " 吃饭");}function Son (name, age, favor, sex) {this.favor = favor // 兴趣爱好this.sex = sexParent.call(this, name, age) // TS继承中使用super}let sonobj2 = new Son("lisi", 34, "打篮球", "男");console.log("sonobj2:", sonobj2)console.log("sonobj2.friends:", sonobj2.friends); //undefined
2. 借用构造函数继承的不足
借用构造函数继承实现了子类构造函数向父类构造函数传递参数,但没有继承父类原型的属性和方法,无法访问父类原型上的属性和方法。
4. 借用构造函数+原型链组合式继承
1. 构造函数+原型链组合式继承优势
优势一 具备借用构造函数的优点:子类的内部可以向父类传递参数
优势二 具备原型链继承的优点:Son.prototype和new Son()创建的实例对象变量可以访问父类原型对象上的属性和方法。
function Parent(name, sex, phone) {this.name = name;this.sex = sex;this.phone = phone;}Parent.prototype.doEat = function () {console.log(this.name + "吃饭...");};function Son(name, sex, phone, nationality) {Parent.call(this, name, sex, phone);this.nationality = nationality;}Son.prototype = new People("default", "male", "0");Son.prototype.constructor = Son;
2. 构造函数+原型链组合式继承的不足
调用了两次父类构造函数带来的问题:
缺点一 进入People构造函数为属性赋值,分配内存空间,浪费内存。
缺点二 赋值导致效率下降一些,关键是new Parent赋值无意义,出现代码冗余。new Son新建的对象和这些值毫不相干,是通过子类Son构造函数中的call向父类Parent构造函数赋值的。
5. 寄生组合式继承
寄生组合式继承 = 借用构造函数继承 + 寄生继承
寄生组合式继承既沿袭了借用构造函数+原型链继承两个优势,而且解决了借用构造函数+原型链继承调用了两次父类构造函数为属性赋值的不足。寄生组合式继承模式保留了借用构造函数继承模式,使用了寄生继承代替了原型链继承。
什么是寄生继承呢?
就是Son.portotype不再指向new Parent()创建的对象空间,而用Parent类的原型对象属性“克隆”了一个对象,再让Son.prototype指向这个新对象,很好地避免了借用构造函数+原型链继承调用了两次父类构造函数的不足。
1. 第一种实现方式
通用性和灵活度更高
function Parent(name, sex, phone) {this.name = name;this.sex = sex;this.phone = phone;}Parent.prototype.doEat = function () {console.log(this.name + "吃饭...");};function Son(name, sex, phone, nationality) {Parent.call(this, name, sex, phone);this.nationality = nationality;}function _extends(parent, son) {// 创建一个寄生构造函数function Middle() {this.constructor = son;}Middle.prototype = parent.prototype;return new Middle();}Son.prototype = _extends(Parent, Son);
2. 第二种实现方式
Son.prototype = Object.create(Parent.prototype);Son.prototype.constructor = Son;
3. 第三种实现方式
仅面试会问到,实际不使用
// 第三种实现方式let cloneParentObj = {};cloneParentObj.__proto__ = Parent.prototype;Son.prototype = cloneParentObj;Son.prototype.constructor = Son;
6. 理解子类
1. 什么是子类?
有两个类,比如A类和B类,如果A类is a kind of B类,那么A类就是B类的子类,比如:A类是顾客类,B类是人类,因为顾客类 is a kind of 人类,所以顾客类是人类的子类。
2. 子类如何继承父类的属性和方法?
以顾客类为例:顾客类继承了父类的非私有的属性和方法,也具备子类独有的属性和方法。
顾客类继承父类的全部非私有的属性和方法,还有哪些独特的属性和方法呢?顾客类独有的属性:顾客等级、顾客编号。顾客类独有的方法:购买
3. 初步理解为什么要用继承?
举例:宠物管理项目中的狗狗类、兔子类、小猫类都是宠物,尽管每个宠物都有独特的属性和方法,比如狗狗类的品种、看家方法;兔子类的肤色属性等。但这些类都包含了 name、buymoney、healthStatus、friendshipStar等属性。如果每一个类都写这些属性,那么就十分臃肿,可以把这些属性集中提取到一个宠物类中,其他类都继承这个宠物类。
4. 汽车租赁管理功能(深度掌握继承)
需求1 汽车租赁功能实现: 有小轿车,大巴,卡车三种类型的车,顾客可以租任意一种或多种不同类型的车,按照租用的天计算租金, 同时为了响应国家对各类车安全的管理, 对在租赁期内有过各种超载,超乘客数,酒后驾车等违规的车需额外支付一定的费用。
需求2 计算退回费用:最终退回顾客的费用为押金扣除使用天数,如押金不足需额外支付不足部分。
小轿车、大巴、卡车共同属性:品牌(brand)、VechileNo(车牌号)、days(租赁天数)、total(支付的租赁总费用)、deposit(押金)
小轿车、大巴、卡车共同方法:计算租赁车的价格(calculateRent)、支付押金的方法(payDesposit)、安全规则方法(safeShow)
7. 方法重写
【条件】 一定发生在继承的子类中
【位置】子类中重写父类的方法
【应用场景】当父类中的方法实现不满足子类功能需要或者不能完全满足子类功能需要时,就需要在子类中进行重写。
【方法重写给继承带来的好处】让所有的子类共用父类中方法已经实现的一部分功能代码。父类方法代码在各个子类中得到了复用。
定义规则
- 和父类方法同名
- 参数和父类相同,如果是引用类型的参数,需要依据具体类型定义
- 父类方法的访问范围必须小于子类中方法重写的范围,而且父类方法不能是private。
super的两种用法
用法一:在子类的构造函数中使用super就代表用来调用父类的构造函数
用法二:在子类重写方法中调用父类同名方法,super重写方法。
错误用法: 当子类和父类有同名属性时,可以在子类中用super来获取父类同名属性吗?不能,一般要避免子类属性和父类属性同名。
8. TS继承编译后的JS源码
1. setPrototypeOf相关技术
Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即内部[[Prototype]]属性)到另一个对象或null。
Object.setPrototypeOf()是ECMAScript 6最新草案中的方法,相对于 Object.prototype.proto(en-US) ,它被认为是修改对象原型更合适的方法
_setPrototypeOf_和_Object.create_的区别
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。创建新对象而不能改变原对象的prototype,这一点和Object.setPrototypeOf()不一样。
Object.setPrototypeOf()不再借用中间的对象空间,就达到了继承的目的。
实现原型链继承
function _extends(son, parent) {return Object.setPrototypeOf(son.prototype, parent.prototype);}
2. 静态成员的继承
第一种方式 key … in 语句
函数以对象形式呈现时,上面自有属性就是静态属性,上面自有方法就是静态方法
for (let key in People) {// 除了自有属性,还会查找__proto__指向的对象空间(这里是rootClass函数对象空间)中自有属性if (Object.prototype.hasOwnProperty.call(People, key)) {// 要求返回true的条件是本构造函数的自有属性// 不会查找__proto__指向的对象空间(这里是rootClass函数对象空间)中自有属性// console.log("key:", key); // 静态属性和静态方法ChinesePeople[key] = People[key]; // 子类ChinesePeople继承父类People的静态属性和静态方法}}
第二种方式 Object.keys()方法
Object.keys(People).forEach((key) => {ChinesePeople[key] = People[key]})
第三种方式 改变proto
这种方法建立更费时,但是可以节省空间。
ChinesePeople.__proto__ = People
第四种方式 ES6新方法Object.setPrototypeOf()
Object.setPrototypeOf(ChinesePeople, People);// 最终建立的关系是ChinesePeople.__proto__ = People
3. 源码分析
let __extends = (function () {function getExtendsStaticWithForIn(son, parent) {for (let key in parent) {if (Object.prototype.hasOwnProperty.call(parent, key)) {son[key] = parent[key];}}}function getExtendsStaticWithProto(son, parent) {son.__proto__ = parent;}let extendsStatics = function () {extendsStatics =Object.setPrototypeOf ||getExtendsStaticWithForIn ||getExtendsStaticWithProto;return extendsStatics(son, parent);};return function (son, parent) {if (typeof parent !== "function" && parent !== null)throw new TypeError("Class extends value " + String(parent) + " is not a ");extendsStatics(son, parent);function Middle() {this.constructor = son;}son.prototype =parent === null? Object.create(null): ((Middle.prototype = parent.prototype), new Middle());};})();
// 手写优化后源码:var _extends = (this.extends_) || (function () {function getExendsStatics2 (son, parent) {son.__proto__ = parent}function getExendsStatics3 (son, parent) {for (let key in parent) {if (Object.prototype.hasOwnProperty.call(parent, key)) {son[key] = parent[key]}}// 等价//继承父类的静态属性和方法// Object.keys(parent).forEach(function (son) {// Child[key] = Father[key];// });//return Object.setPrototypeOf(son, parent)}var extendsStatics = function (son, parent) {extendsStatics = Object.setPrototypeOf || getExendsStatics2 || getExendsStatics3return extendsStatics(son, parent)}var _extends = function (son, parent) {extendsStatics(son, parent)function middle () {this.constructor = son}if (parent) {middle.prototype = parent.prototype//son.prototype = parent === null ? Object.create(null) : new middle()son.prototype = new Middle();} else {son.prototype = Object.create(null)}}return _extends;})()
使用
var Vechile = (function () {function Vechile (brand_, vechileNo_, days_, deposit_) {this.brand = brand_;this.vechileNo = vechileNo_;this.days = days_;this.deposit = deposit_;}// 计算租赁车的价格 ( calculateRent)Vechile.prototype.calculateRent = function () {console.log("calculateRent来自Vechile=>this.brand:", this.brand);console.log(this.brand + " 车牌号为:" + this.vechileNo + "开始被租");return 0;};Vechile.prototype.safeShow = function () {console.log("车规则....");console.log(this.brand + " 车牌号为:" + this.vechileNo + " 违规了:");};Vechile.count = 300;return Vechile;}())var Car = (function (_super) {_extends(Car, _super)function Car (brand_, vechileNo_, days_, deposit_, type_) {_super.call(this, brand_, vechileNo_, days_, deposit_)// var _this = _super.call(this, brand_, vechileNo_, days_, deposit_) || thisthis.type = type_;//console.log("_this:", _this)//_this.type = type_;//return _this;return this;}Car.prototype.getPriceByType = function () {var rentMoneyByDay = 0; //每天的租金if (this.type === "普拉多巡洋舰") {rentMoneyByDay = 800;}else if (this.type === "凯美瑞旗舰版") {rentMoneyByDay = 400;}else if (this.type === "威驰智行版") {rentMoneyByDay = 200;}return rentMoneyByDay;};Car.prototype.calculateRent = function () {this.safeShow();_super.prototype.calculateRent.call(this); //=Vechile.prototype.calculateRent.call(this)console.log("Car:", Car.count);console.log("型号:", this.type)return this.days * this.getPriceByType();};return Car;}(Vechile))var car = new Car("普拉多", "京3A556", 3, 100000, "凯美瑞旗舰版");console.log("car:", car)console.log(car.calculateRent());
4. 类型断言与转换
1. 类型断言的定义
把两种有重叠关系的数据类型进行相互转换的一种TS语法,把其中的一种数据类型转换成另外一种数据类型。类型断言和类型转换产生的效果一样,但语法格式不同。
【语法格式】:A数据类型的变量 as B数据类型的变量
其中A数据类型和B数据类型必须具有重叠关系。
2.理解重叠关系
- 如果 A,B 如果是类并且有继承关系。无论 A,B 谁是父类或子类, A 的对象变量可以断言成 B 类型,B 的对象变量可以断言成A类型 。但注意一般在绝大多数场景下都是把父类的对象变量断言成子类。
- 如果 A,B 如果是类,但没有继承关系。两个类中的任意一个类的所有的 public 实例属性(不包括静态属性)加上所有的 public 实例方法和另一个类的所有 public 实例属性加上所有的 public 实例方法完全相同或是另外一个类的子集,则这两个类可以相互断言,否则这两个类就不能相互断言。
- 如果 A 是类,B 是接口,并且 A 类实现了 B 接口,则 A 的对象变量可以断言成 B 接口类型,同样 B 接口类型的对象变量也可以断言成A类型 。
- 如果 A 是类,B 是接口,并且 A 类没有实现了 B 接口,则断言关系和第2项的规则完全相同。
- 如果 A 是类,B 是 type 定义的数据类型(就是引用数据类型,例如 Array 对象,不能是基本数据类型,例如 string,number, boolean),并且有 A 类实现了 B type 定义的数据类型【implements】,则 A 的对象变量可以断言成 B type 定义的对象数据类型,同样 B type 定义的对象数据类型的对象变量也可以断言成A类型。
- 如果 A 是类,B 是 type 定义的数据类型,并且 A 类没有实现 B type定义的数据类型,则断言关系和第2项的规则完全相同。
- 如果 A 是一个函数上参数变量的联合类型,例如
string | number,那么在函数内部可以断言成 string 或number 类型。 - 多个类组成的联合类型如何断言?例如:
let vechile: Car | Bus | Trunck。 vechile 可以断言成其中任意一种数据类型。 例如vechile as Car, vechile as Bus , vechile as Trunck。 - 任何数据类型都可以转换成 any 或 unknown 类型,any 或 unknown 类型也可以转换成任何其他数据类型。
3. 类型断言存在的意义
场景一:顾客在执行汽车租赁项目租赁价格计算方法中调用每一个类的独有方法时使用。
场景二:对象中的Symbol数据类型取值
场景三:加法计算,巧用as any
let symid = Symbol("objid")let obj = { [symid]: 101, username: "wangwu", age: 23 }let username = obj["username"]// let objid = obj[symid] //类型“symbol”不能作为索引类型使用// 解决:let objid = obj[symid as any]let objid2 = obj[symid as unknown] //类型“unknown”不能作为索引类型使用let symidunknown = symid as unknown // 可以转换成unknown,正确
5. 类型守卫
1. new的底层发生了什么?
function Person(phone, age) {this.age = age; //age实例属性this.phone = phone; //phone实例属性this.showone = function () {}; //showone实例方法}Person.prototype.doEat = function () {console.log("电话:", this.phone);};let person = new Person("12344", 23);
new 一个实例对象的底层 3步
- 创建一个 Object 对象。
let obj = new Object()或者let obj = {} - 让新创建的对象的 proto 变量指向 Person 原型对象空间。
obj.__proto__=Person.prototype; - 借用Person构造函数中的为 obj 对象变量增加 age 属性和 phone 属性。
Person.apply(obj,["12344",23]);
2. 类型守卫的定义
定义:在语句的块级作用域(if语句内或者条目运算符表达式内)缩小变量的一种类型推断的行为。
产生时机:TS条件语句中遇到下列关键字时,会在语句的块级作用域内缩小变量的类型,这种类型推断的行为称为类型守卫Type Guard。类型守卫可以帮助我们在块级作用域中获得更为需要的精确变量类型,从而减少不必要的类型断言。
- 类型判断:
typeof - 属性或者方法或者函数判断:
in - 实例判断:
instanceof - 字面量相等判断:
==``===``!=``!==
3. typeof
1. typeof 作用
typeof 用来检测一个变量或一个对象的数据类型。
typeof 检测的范围:typeof 检测变量的类型范围包括:string|number|bigint|symbol|undefined|object|function等数据类型。
2. typeof 的局限性
typeof检测变量并不完全准确,例如
typeof null结果为object,这其实设计者的一个 bug,但后来一直没有被改过来,于是就此传下来。把 null 当成 object 的理由说成是未来可能会拥有一个对象空间,这个理由很牵强(我们检测的是对象变量此刻的类型),null 本来即是数据类型,也是值。所以 typeof null 直接显示 null 最合适了。
再例如:使用 typeof 来检测一个数组变量,typeof [ ] 结果显示的是 object,从 Array 创建的本质上来说确实是 object,正如我们在 2-29-1中所讲,但开发者期待看到的是 Array,这更符合预期。Array和我们定义的普通函数一样,具有双重性,当成函数类型时用来创建数组对象,但也是一个构造函数对象,拥有静态成员和prototype原型对象属性。
再比如:使用 typeof 来检测一个 Set 变量,Map 变量,结果显示的是依然是 object。
3. typeof 的替代方案
Object.prototype.toString.call
Object.prototype.toString.call ([ ]) 展示[ object Array ]
Object.prototype.toString.call(null) 展示[ object null ]
Object.prototype.toString.call(Set类型的变量) 展示[ object Set ]
Object.prototype.toString.call(Map类型的变量) 展示[ object Map ]
typeof 的替代方案依然无法解决的问题
就是无法获取一个自定义的类的实例变量或构造函数的对象变量的真正创建类型,答案是使用 instanceof 来解决。4. 类型守卫考核题
请编写一个操作对象方法和属性的函数实现以下功能:
当对象字符串属性有空格时就去掉空格后输出。
- 当遇到对象方法时就执行,其他数据类型的属性一律直接输出。
- 只有对象中包含allowoutput属性时,才允许输出。
- 函数接到外部传入的
null,undefined,{}时,直接输出不是一个合法的对象。 ```typescript interface TestType { username: string; age: number; eat(): void; allowinput?: 1; }
let testobj: TestType = { username: “wan g wu”, age: 23, eat() { console.log(“this:”); console.log(this.username + “ 吃饭”); }, allowinput: 1, };
class StringUtil { public static trimSpace(str: string): string { return str.replace(/\s+/g, “”); } }
function processObjOutput(obj: any) { if (obj && “allowinput” in obj) { let value; Object.keys(obj).forEach((key) => { value = obj[key]; if (typeof value === “string”) { obj[key] = StringUtil.trimSpace(value); console.log(key + “:”, obj[key]); } else if (typeof value === “function”) { objkey; } else { console.log(key + “:”, value); } }); } else { console.log(“不是一个合法的对象”); } }
processObjOutput({});
<a name="oE8bz"></a>## 4. instanceof<a name="GyJIO"></a>### 1. 基本使用**instanceof 格式**:`对象变量 instanceof 类名或函数名`<br />**instanceof 的主要作用:** instanceof 帮助我们_准确的判断_一种自定义函数或类创建的_对象变量的数据类型_。<br />**instanceof 执行后返回 true 的两种条件(符合一个即可)**1. 对象变量.__proto__=类名或函数名.prototype <br />解释1:如果 instanceof 关键字 左边对象变量的__proto__属性指向的原型对象空间=右边类名或函数名的 prototype 对象属性指向的原型对象空间,那么 返回 true。1. 对象变量.proto.proto...._proto__=类名或函数名.prototype。<br />解释2:instanceof 左边对象变量__proto__的1到多个上一级__proto__指向的原型对象空间,等于右边类名或函数名的 prototype 对象属性指向的原型对象空间,那么也返回 true,按照这个查找规律,会一直找到Object.prototype 对象属性指向的原型对象空 间为止。<a name="wBDCl"></a>### 2. 真实应用场景顾客租赁多辆不同类型的车后如何计算租赁价格。- 【具体需求1】汽车租赁功能实现: 有小轿车、大巴、卡车三种类型的车,顾客可以租任意一种或多种不同类型的车,按照租用的天计算租金,同时为了响应国家对各类车安全的管理,对在租赁期内有过各种超载,超乘客数,酒后驾车等违规的车需额外支付一定的费用。- 【具体需求2】计算退回费用:最终退回顾客的费用为押金扣除使用天数,如押金不足需额外支付不足部分。<a name="sedUC"></a>## 5. TS多态+类型守卫【多态的定义】父类的对象变量可以接受任何一个子类的对象,从而用这个父类的对象变量来调用子类中重写的方法而输出不同的结果。<br />【多态产生的条件】1. 必须存在继承关系 2. 必须有方法重写,没有方法重写就产生不了多态。<br />【多态的好处】利于项目的扩展,从局部满足了开闭原则:对修改关闭,对扩展开放。<br />【多态的局限性】无法调用子类的独有方法,必须结合instanceof类型守卫来解决。实例1:```typescriptclass People {public name!: string;public eat() {console.log("People父类的eat");}}class American extends People {// 美国人public phone!: string;public eat() {console.log("用叉子吃饭...");}}class Chinese extends People {//中国人public eat() {console.log("用筷子吃饭...");}}class Indian extends People {// 土族人public eat() {console.log("用手抓吃饭...");}}// 父类的对象变量people可以接受任何一个子类的对象,// 例如可以接受American,Chinese,Indian子类对象let people: People = new American();// 从而用这个父类的对象变量来调用子类中重写的方法而输出不同的结果.people.eat();people = new Chinese();people.eat();people = new Indian();people.eat();
实例2:
class Customer {rentVechile(vechile: Vechile) {if (vechile instanceof Car) {vechile.checkIsWeigui(true);} else if (vechile instanceof Bus) {vechile.checkIsOverNum(true);} else if (vechile instanceof Truck) {vechile.checkIsOverWeight(true)}return vechile.calculateRent();}}let cust = new Customer();let car = new Car("普拉多", "京3A556", 3, 100000, "凯美瑞旗舰版");console.log(cust.rentVechile(car));
6. 抽象类
【定义】一个在任何位置都不能被实例化的一个类就是抽象类,抽象类只能通过子类来实例化。
1. 什么样的类可以被定义为抽象类?
从宏观上来说,任何一个实例化后毫无意义的类 都可以定义为抽象类。比如:我们实例化一个玫瑰花类的对象变量,可以得到一个具体的玫瑰花实例对象,但如果我们实例化一个Flower类的对象变量,那世界上有一个叫“花”的对象吗?很明显没有,所以Flower类可以定义为一个抽象类,但玫瑰花可以定义为具体的类。
2. 一个类定义为抽象类的样子
abstract class 类名{}
- 可以有0到多个抽象方法,只有方法体,没有方法实现的方式。
- 可以有0到多个具体方法,可以有构造器,可以有0到多个实例属性,0到多个静态属性,0到多个静态方法。
单纯从类的定义上来看和普通的类没有区别,只是多了可以有0到多个抽象方法这一条,并且不能被实例化。
3. 抽象类的特点
可以包含方法声明的方法(抽象方法:和方法签名类似,就是多了abstract关键字,没有方法体)。
- 也可以包含实现了具体功能的方法。
- 可以包含构造器,但不能直接实例化一个抽象类,只能通过子类来实例化。
4. 抽象类比普通类充当父类给项目带来的好处
【好处一】提供统一名称的抽象方法,提高代码的可维护性。抽象类通常用来充当父类,当抽象类把一个方法定义为抽象方法,那么会强制在所有子类中实现它,防止不同子类的同功能的方法命名不相同,从而降低项目维护的成本。
【好处二】防止实例化一个实例化后毫无意义的类 ```typescript abstract class People { //抽象类 constructor() {} public name: string; abstract eat(): void; //抽象方法 特点 1:没有方法体 2:带abstract关键字 public step() { console.log(“双腿走路”); } } // let people2 = new People(); //无法创建抽象类的实例
class American extends People { eat(): void { throw new Error(“Method not implemented.”); } }
class Chinese extends People { //中国人 public eat() { console.log(“用筷子吃饭…”); } }
class TuzhuPeople extends People { // 土族人 public eat() { console.log(“用手抓吃饭…”); } }
let people: People = new American();
export {};
<a name="DkBxK"></a>### 5.抽象类和接口结合的真实应用场景1. 抽象类和接口相结合,相对于过于抽象的接口,起到缓冲的作用。1. 起到提示作用。比如,将不常用的方法设置为具体方法,而将常用方法设置为抽象方法,利于自动提示功能,可以自动生成子类必需的方法。<br />以下的适配器就是一个抽象类:```typescriptinterface MouseListenerProcess {mouseRealeased(e: any): void; //鼠标按钮在组件上释放时调用。mousePressed(e: any): void; // 鼠标按钮在组件上按下时调用mouseEntered(e: any): void; // 鼠标进入组件时调用mouseClicked(e: any): void; // 鼠标按键在组建上单击(按下并释放)时调用mouseExited(e: any): void; // 鼠标离开组件时调用}abstract class MyMouseListenerProcessAdapter implements MouseListenerProcess {mouseRealeased(e: any): void {throw new Error("Method not implemented.");}mousePressed(e: any): void {throw new Error("Method not implemented.");}mouseEntered(e: any): void {throw new Error("Method not implemented.");}abstract mouseClicked(e: any): void;abstract mouseExited(e: any): void;}class MyMouseListenerProcess extends MyMouseListenerProcessAdapter {mouseClicked(e: any): void {throw new Error("Method not implemented.");}mouseExited(e: any): void {throw new Error("Method not implemented.");}}
7. 自定义守卫
定义:自定义守卫是通过{形参 is 类型}返回布尔值的赋值类型守卫的能力的条件表达式{形参 is 类型}中的类型可以是:接口、类、基础数据类型。
自定义守卫的格式:
function 函数名(形参: 参数类型(大多为any)) : 形参 is A类型 {return true or false}例如:function isString(str: any): str is string {return typeof str === "string";}
理解:返回布尔值的条件表达式赋予类型守卫的能力,只有当函数返回true时,形参被确定为A类型。
vue3源码中的自定义守卫
isRef中的自定义守卫r is Ref中r是形参,固定不变,Ref是一种数据类型
//export interface Ref<T = any> {// 泛型先不用管export interface Ref {value: any,// key为Symbol的属性做类型标识_shallow?: boolean}export function isRef(r: any): r is Ref {// r is Ref 效果等同于booleanreturn Boolean(r && r.__v_isRef === true)}// 没有用泛型的unref,泛型大家先不用管,接下来几章我们会非常详细的讲解export function unref(ref: unknown) {return isRef(ref) ? (ref.value as any) : ref}
自定义守卫理解:
- 如果unref方法中调用的if语句或条件运算符中的
isRef(ref)方法返回true,那么is语句块或条件运算符:号后面的ref就被编译器解析为Ref类型的变量。 r is Ref会对r变量进行守卫(把类型的范围缩小,这里的r被缩小为Ref类型)
8. TypeScript4新特性
1. const 为何也能被修改?如何解决?
//const arr=[10,30,40,"abc"]//arr=[100,30,40,"abc"]//arr[0]=100const arr = [10, 30, 40, "abc"] as const//arr = [100, 30, 40, "abc"]//arr[0] = 100; // 错误 无法分配到"数组的索引为0位置的元素",因为它是只读属性// 类型“readonly any[]”中的索引签名仅允许读取,必须加readonly才能使用[] as const类型的数组function showArr(arr: readonly any[]) {//arr[0] = 100;console.log(arr)}showArr(arr)
2. 为什么要用可变元组+元组标签+具体使用
// 可变元组// let [username, age]: [string, number,string,string,string] = ["wangwu", 23,// "海口海淀岛四东路3号", "133123333", "一路同行,一起飞"]// let [username, age]: [string, number, ...any[]] = ["wangwu", 23,// "海口海淀岛四东路3号", "133123333", "一路同行,一起飞",23,"df"]// let [username, age, ...rest]: [string, number, ...any[]] = ["wangwu", 23,// "海口海淀岛四东路3号", "133123333", "一路同行,一起飞", 23, "df"]// [ '海口海淀岛四东路3号', '133123333', '一路同行,//一起飞', 23, 'df' ]// 元组标签,标签增强可读性let [username, age, ...rest]: [name_: string, age_: number, ...rest: any[]] = ["wangwu",23,"海口海淀岛四东路3号","133123333","一路同行,一起飞",23,"df",];console.log("username:", username); //wangwuconsole.log("age:", age); //23console.log("rest:", rest); //rest: [ '海口海淀岛四东路3号', '133123333', '一路同行,一起飞', 23, 'df' ]
典型的错误:
const arr: (string | number)[] = [10, 30, 40, "abc", 30] as const// 类型 "readonly [10, 30, 40, "abc", 30]" 为 "readonly",// 不能分配给可变类型 "(string | number)[]"
